[Response Ops][Connectors] Allow connectors to explicitly register which features they will be available in (#136331)

* Adding feature config to connector type and checking for validity on registration

* Updating actions APIs to filter by feature id if provided

* Fixing types

* Renaming allowedFeatureIds to featureConfig

* Adding siem feature config. Returning feature config to client. Showing availability in connector list

* Fixing types

* Showing availability in create connector flyout header

* Passing feature id into action form used by rule creators.

* Renaming some stuff

* Finishing triggers_actions_uis. Starting cases

* Fixing cases

* fixing types

* Fixing types and adding uptime feature

* Cleanup

* fixing tests

* Updating README

* Filtering action type filter on rule list

* Update x-pack/plugins/actions/common/connector_feature_config.ts

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>

* Fixing tests

* Renaming featureConfig to supportedFeatureIds

* PR feedback

* fixing i18n

* Updating docs

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ying Mao 2022-07-25 09:24:25 -04:00 committed by GitHub
parent 676be86d9d
commit 3c24511c16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 1157 additions and 514 deletions

View file

@ -26,6 +26,12 @@ run this API.
`space_id`::
(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used.
[[list-connector-types-api-query-params]]
=== {api-query-parms-title}
`feature_id`::
(Optional, string) Filters list of connector types to those that support the feature id.
[[list-connector-types-api-codes]]
==== {api-response-codes-title}
@ -52,7 +58,8 @@ The API returns the following:
"minimum_license_required": "gold", <3>
"enabled": false, <4>
"enabled_in_config": true, <5>
"enabled_in_license": true <6>
"enabled_in_license": true, <6>
"supported_feature_ids": ["alerting"] <7>
},
{
"id": ".index",
@ -60,7 +67,8 @@ The API returns the following:
"minimum_license_required": "basic",
"enabled": true,
"enabled_in_config": true,
"enabled_in_license": true
"enabled_in_license": true,
"supported_feature_ids": ["alerting"]
},
...
]
@ -71,3 +79,4 @@ The API returns the following:
<4> `enabled` - Specifies if the connector type is enabled or disabled in {kib}.
<5> `enabled_in_config` - Specifies if the connector type is enabled or enabled in the {kib} `.yml` file.
<6> `enabled_in_license` - Specifies if the connector type is enabled or disabled in the license.
<7> `supported_feature_ids` - Specifies which Kibana features this connector type supports.

View file

@ -133,6 +133,7 @@ The following table describes the properties of the `options` object.
| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string |
| maxAttempts | The maximum number of times this action will attempt to run when scheduled. | number |
| minimumLicenseRequired | The license required to use the action type. | string |
| supportedFeatureIds | List of IDs of the features that this action type is available in. Allowed values are `alerting`, `siem`, `uptime`, `cases`. See `x-pack/plugins/actions/common/connector_feature_config.ts` for the most up to date list. | string[] |
| validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example `to`, `from`, `subject`, `body` of an email). See the current built-in email action type for an example of the state-of-the-art validation. <p>Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function |
| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function |
| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function |

View file

@ -0,0 +1,37 @@
/*
* 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 { areValidFeatures, getConnectorFeatureName } from './connector_feature_config';
describe('areValidFeatures', () => {
it('returns true when all inputs are valid features', () => {
expect(areValidFeatures(['alerting', 'cases'])).toBeTruthy();
});
it('returns true when only one input and it is a valid feature', () => {
expect(areValidFeatures(['alerting'])).toBeTruthy();
expect(areValidFeatures(['cases'])).toBeTruthy();
});
it('returns false when one item in input is invalid', () => {
expect(areValidFeatures(['alerting', 'nope'])).toBeFalsy();
});
it('returns false when all items in input are invalid', () => {
expect(areValidFeatures(['alerts', 'nope'])).toBeFalsy();
});
});
describe('getConnectorFeatureName', () => {
it('returns the feature name for valid feature ids', () => {
expect(getConnectorFeatureName('siem')).toEqual('Security Solution');
});
it('returns the id for invalid feature ids', () => {
expect(getConnectorFeatureName('foo')).toEqual('foo');
});
});

View file

@ -0,0 +1,73 @@
/*
* 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 { i18n } from '@kbn/i18n';
interface ConnectorFeatureConfig {
/**
* Unique identifier for this feature.
*/
id: string;
/**
* Display name for this feature.
* This will be displayed to end-users, so a translatable string is advised for i18n.
*/
name: string;
}
export const AlertingConnectorFeatureId = 'alerting';
export const CasesConnectorFeatureId = 'cases';
export const UptimeConnectorFeatureId = 'uptime';
export const SecurityConnectorFeatureId = 'siem';
export const AlertingConnectorFeature: ConnectorFeatureConfig = {
id: AlertingConnectorFeatureId,
name: i18n.translate('xpack.actions.availableConnectorFeatures.alerting', {
defaultMessage: 'Alerting',
}),
};
export const CasesConnectorFeature: ConnectorFeatureConfig = {
id: CasesConnectorFeatureId,
name: i18n.translate('xpack.actions.availableConnectorFeatures.cases', {
defaultMessage: 'Cases',
}),
};
export const UptimeConnectorFeature: ConnectorFeatureConfig = {
id: UptimeConnectorFeatureId,
name: i18n.translate('xpack.actions.availableConnectorFeatures.uptime', {
defaultMessage: 'Uptime',
}),
};
export const SecuritySolutionFeature: ConnectorFeatureConfig = {
id: SecurityConnectorFeatureId,
name: i18n.translate('xpack.actions.availableConnectorFeatures.securitySolution', {
defaultMessage: 'Security Solution',
}),
};
const AllAvailableConnectorFeatures: ConnectorFeatureConfig[] = [
AlertingConnectorFeature,
CasesConnectorFeature,
UptimeConnectorFeature,
SecuritySolutionFeature,
];
export function areValidFeatures(ids: string[]) {
return ids.every(
(id: string) =>
!!AllAvailableConnectorFeatures.find((config: ConnectorFeatureConfig) => config.id === id)
);
}
export function getConnectorFeatureName(id: string) {
const featureConfig = AllAvailableConnectorFeatures.find((config) => config.id === id);
return featureConfig ? featureConfig.name : id;
}

View file

@ -14,6 +14,7 @@ export * from './rewrite_request_case';
export * from './mustache_template';
export * from './validate_email_addresses';
export * from './servicenow_config';
export * from './connector_feature_config';
export const BASE_ACTION_API_PATH = '/api/actions';
export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions';

View file

@ -14,6 +14,7 @@ export interface ActionType {
enabledInConfig: boolean;
enabledInLicense: boolean;
minimumLicenseRequired: LicenseType;
supportedFeatureIds: string[];
}
export enum InvalidEmailReason {

View file

@ -59,6 +59,7 @@ describe('register()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
executor,
});
expect(actionTypeRegistry.has('my-action-type')).toEqual(true);
@ -86,6 +87,7 @@ describe('register()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
};
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
@ -100,6 +102,7 @@ describe('register()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
expect(() =>
@ -107,6 +110,7 @@ describe('register()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
})
).toThrowErrorMatchingInlineSnapshot(
@ -114,12 +118,43 @@ describe('register()', () => {
);
});
test('throws if empty supported feature ids provided', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
expect(() =>
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: [],
executor,
})
).toThrowErrorMatchingInlineSnapshot(
`"At least one \\"supportedFeatureId\\" value must be supplied for connector type \\"my-action-type\\"."`
);
});
test('throws if invalid feature ids provided', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
expect(() =>
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['foo'],
executor,
})
).toThrowErrorMatchingInlineSnapshot(
`"Invalid feature ids \\"foo\\" for connector type \\"my-action-type\\"."`
);
});
test('provides a getRetry function that handles ExecutorError', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1);
@ -140,6 +175,7 @@ describe('register()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
executor,
});
expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith(
@ -154,6 +190,7 @@ describe('register()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
expect(actionTypeRegistryParams.licensing.featureUsage.register).not.toHaveBeenCalled();
@ -167,6 +204,7 @@ describe('get()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
const actionType = actionTypeRegistry.get('my-action-type');
@ -176,6 +214,9 @@ describe('get()', () => {
"id": "my-action-type",
"minimumLicenseRequired": "basic",
"name": "My action type",
"supportedFeatureIds": Array [
"alerting",
],
}
`);
});
@ -196,6 +237,7 @@ describe('list()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
const actionTypes = actionTypeRegistry.list();
@ -207,6 +249,40 @@ describe('list()', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalled();
expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalled();
});
test('returns list of connector types filtered by feature id if provided', () => {
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
actionTypeRegistry.register({
id: 'another-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['cases'],
executor,
});
const actionTypes = actionTypeRegistry.list('alerting');
expect(actionTypes).toEqual([
{
id: 'my-action-type',
name: 'My action type',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalled();
@ -226,6 +302,7 @@ describe('has()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
expect(actionTypeRegistry.has('my-action-type'));
@ -238,6 +315,7 @@ describe('isActionTypeEnabled', () => {
id: 'foo',
name: 'Foo',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: async (options) => {
return { status: 'ok', actionId: options.actionId };
},
@ -305,6 +383,7 @@ describe('ensureActionTypeEnabled', () => {
id: 'foo',
name: 'Foo',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: async (options) => {
return { status: 'ok', actionId: options.actionId };
},
@ -350,6 +429,7 @@ describe('isActionExecutable()', () => {
id: 'foo',
name: 'Foo',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: async (options) => {
return { status: 'ok', actionId: options.actionId };
},

View file

@ -9,7 +9,7 @@ import Boom from '@hapi/boom';
import { i18n } from '@kbn/i18n';
import { RunContext, TaskManagerSetupContract } from '@kbn/task-manager-plugin/server';
import { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
import { ActionType as CommonActionType } from '../common';
import { ActionType as CommonActionType, areValidFeatures } from '../common';
import { ActionsConfigurationUtilities } from './actions_config';
import {
ExecutorError,
@ -122,6 +122,31 @@ export class ActionTypeRegistry {
)
);
}
if (!actionType.supportedFeatureIds || actionType.supportedFeatureIds.length === 0) {
throw new Error(
i18n.translate('xpack.actions.actionTypeRegistry.register.missingSupportedFeatureIds', {
defaultMessage:
'At least one "supportedFeatureId" value must be supplied for connector type "{connectorTypeId}".',
values: {
connectorTypeId: actionType.id,
},
})
);
}
if (!areValidFeatures(actionType.supportedFeatureIds)) {
throw new Error(
i18n.translate('xpack.actions.actionTypeRegistry.register.invalidConnectorFeatureIds', {
defaultMessage: 'Invalid feature ids "{ids}" for connector type "{connectorTypeId}".',
values: {
connectorTypeId: actionType.id,
ids: actionType.supportedFeatureIds.join(','),
},
})
);
}
this.actionTypes.set(actionType.id, { ...actionType } as unknown as ActionType);
this.taskManager.registerTaskDefinitions({
[`actions:${actionType.id}`]: {
@ -170,16 +195,21 @@ export class ActionTypeRegistry {
}
/**
* Returns a list of registered action types [{ id, name, enabled }]
* Returns a list of registered action types [{ id, name, enabled }], filtered by featureId if provided.
*/
public list(): CommonActionType[] {
return Array.from(this.actionTypes).map(([actionTypeId, actionType]) => ({
id: actionTypeId,
name: actionType.name,
minimumLicenseRequired: actionType.minimumLicenseRequired,
enabled: this.isActionTypeEnabled(actionTypeId),
enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId),
enabledInLicense: !!this.licenseState.isLicenseValidForActionType(actionType).isValid,
}));
public list(featureId?: string): CommonActionType[] {
return Array.from(this.actionTypes)
.filter(([_, actionType]) =>
featureId ? actionType.supportedFeatureIds.includes(featureId) : true
)
.map(([actionTypeId, actionType]) => ({
id: actionTypeId,
name: actionType.name,
minimumLicenseRequired: actionType.minimumLicenseRequired,
enabled: this.isActionTypeEnabled(actionTypeId),
enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId),
enabledInLicense: !!this.licenseState.isLicenseValidForActionType(actionType).isValid,
supportedFeatureIds: actionType.supportedFeatureIds,
}));
}
}

View file

@ -154,6 +154,7 @@ describe('create()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
@ -186,6 +187,7 @@ describe('create()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
@ -226,6 +228,7 @@ describe('create()', () => {
id: savedObjectCreateResult.attributes.actionTypeId,
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
@ -264,6 +267,7 @@ describe('create()', () => {
id: savedObjectCreateResult.attributes.actionTypeId,
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
@ -316,6 +320,7 @@ describe('create()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
@ -359,6 +364,7 @@ describe('create()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
validate: {
config: schema.object({
param1: schema.string(),
@ -391,6 +397,7 @@ describe('create()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
validate: {
connector: connectorValidator,
},
@ -430,6 +437,7 @@ describe('create()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
@ -562,6 +570,7 @@ describe('create()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
@ -596,6 +605,7 @@ describe('create()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => {
@ -1675,6 +1685,7 @@ describe('update()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
@ -1778,6 +1789,7 @@ describe('update()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
@ -1850,6 +1862,7 @@ describe('update()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
@ -1915,6 +1928,7 @@ describe('update()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
validate: {
config: schema.object({
param1: schema.string(),
@ -1949,6 +1963,7 @@ describe('update()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
validate: {
connector: () => {
return '[param1] is required';
@ -1983,6 +1998,7 @@ describe('update()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
@ -2063,6 +2079,7 @@ describe('update()', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
});
mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => {
@ -2314,6 +2331,7 @@ describe('isActionTypeEnabled()', () => {
id: 'foo',
name: 'Foo',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};
beforeEach(() => {

View file

@ -669,8 +669,8 @@ export class ActionsClient {
return this.ephemeralExecutionEnqueuer(this.unsecuredSavedObjectsClient, options);
}
public async listTypes(): Promise<ActionType[]> {
return this.actionTypeRegistry.list();
public async listTypes(featureId?: string): Promise<ActionType[]> {
return this.actionTypeRegistry.list(featureId);
}
public isActionTypeEnabled(

View file

@ -11,14 +11,19 @@ import { schema, TypeOf } from '@kbn/config-schema';
import nodemailerGetService from 'nodemailer/lib/well-known';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import { Logger } from '@kbn/core/server';
import { withoutMustacheTemplate } from '../../common';
import {
AlertingConnectorFeatureId,
AdditionalEmailServices,
withoutMustacheTemplate,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../common';
import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email';
import { portSchema } from './lib/schemas';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { ActionsConfigurationUtilities } from '../actions_config';
import { renderMustacheString, renderMustacheObject } from '../lib/mustache_renderer';
import { AdditionalEmailServices } from '../../common';
export type EmailActionType = ActionType<
ActionTypeConfigType,
@ -213,6 +218,11 @@ export function getActionType(params: GetActionTypeParams): EmailActionType {
name: i18n.translate('xpack.actions.builtin.emailTitle', {
defaultMessage: 'Email',
}),
supportedFeatureIds: [
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: schema.object(ConfigSchemaProps, {
validate: curry(validateConfig)(configurationUtilities),

View file

@ -11,7 +11,13 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { Logger } from '@kbn/core/server';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { renderMustacheObject } from '../lib/mustache_renderer';
import { buildAlertHistoryDocument, AlertHistoryEsIndexConnectorId } from '../../common';
import {
buildAlertHistoryDocument,
AlertHistoryEsIndexConnectorId,
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../common';
import { ALERT_HISTORY_PREFIX } from '../../common/alert_history_schema';
export type ESIndexActionType = ActionType<ActionTypeConfigType, {}, ActionParamsType, unknown>;
@ -60,6 +66,11 @@ export function getActionType({ logger }: { logger: Logger }): ESIndexActionType
name: i18n.translate('xpack.actions.builtin.esIndexTitle', {
defaultMessage: 'Index',
}),
supportedFeatureIds: [
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: ConfigSchema,
params: ParamsSchema,

View file

@ -32,6 +32,12 @@ import {
ExecutorSubActionGetIncidentParams,
} from './types';
import * as i18n from './translations';
import {
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../../common';
export type ActionParamsType = TypeOf<typeof ExecutorParamsSchema>;
interface GetActionTypeParams {
@ -64,6 +70,12 @@ export function getActionType(
id: ActionTypeId,
minimumLicenseRequired: 'gold',
name: i18n.NAME,
supportedFeatureIds: [
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: schema.object(ExternalIncidentServiceConfiguration, {
validate: curry(validate.config)(configurationUtilities),

View file

@ -13,6 +13,11 @@ import { Logger } from '@kbn/core/server';
import { postPagerduty } from './lib/post_pagerduty';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { ActionsConfigurationUtilities } from '../actions_config';
import {
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../common';
// uses the PagerDuty Events API v2
// https://v2.developer.pagerduty.com/docs/events-api-v2
@ -142,6 +147,11 @@ export function getActionType({
name: i18n.translate('xpack.actions.builtin.pagerdutyTitle', {
defaultMessage: 'PagerDuty',
}),
supportedFeatureIds: [
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: schema.object(configSchemaProps, {
validate: curry(validateActionTypeConfig)(configurationUtilities),

View file

@ -30,6 +30,11 @@ import {
ExecutorSubActionCommonFieldsParams,
} from './types';
import * as i18n from './translations';
import {
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../../common';
export type ActionParamsType = TypeOf<typeof ExecutorParamsSchema>;
@ -55,6 +60,11 @@ export function getActionType(
id: ActionTypeId,
minimumLicenseRequired: 'platinum',
name: i18n.NAME,
supportedFeatureIds: [
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: schema.object(ExternalIncidentServiceConfiguration, {
validate: curry(validate.config)(configurationUtilities),

View file

@ -12,6 +12,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { Logger, LogMeta } from '@kbn/core/server';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { withoutControlCharacters } from './lib/string_utils';
import { AlertingConnectorFeatureId, UptimeConnectorFeatureId } from '../../common';
export type ServerLogActionType = ActionType<{}, {}, ActionParamsType>;
export type ServerLogActionTypeExecutorOptions = ActionTypeExecutorOptions<
@ -48,6 +49,7 @@ export function getActionType({ logger }: { logger: Logger }): ServerLogActionTy
name: i18n.translate('xpack.actions.builtin.serverLogTitle', {
defaultMessage: 'Server log',
}),
supportedFeatureIds: [AlertingConnectorFeatureId, UptimeConnectorFeatureId],
validate: {
params: ParamsSchema,
},

View file

@ -55,6 +55,12 @@ import { throwIfSubActionIsNotSupported } from './utils';
import { createExternalServiceITOM } from './service_itom';
import { apiITOM } from './api_itom';
import { createServiceWrapper } from './create_service_wrapper';
import {
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../../common';
export {
ServiceNowITSMActionTypeId,
@ -92,6 +98,12 @@ export function getServiceNowITSMActionType(
id: ServiceNowITSMActionTypeId,
minimumLicenseRequired: 'platinum',
name: i18n.SERVICENOW_ITSM,
supportedFeatureIds: [
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: schema.object(ExternalIncidentServiceConfiguration, {
validate: curry(validate.config)(configurationUtilities),
@ -120,6 +132,11 @@ export function getServiceNowSIRActionType(
id: ServiceNowSIRActionTypeId,
minimumLicenseRequired: 'platinum',
name: i18n.SERVICENOW_SIR,
supportedFeatureIds: [
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: schema.object(ExternalIncidentServiceConfiguration, {
validate: curry(validate.config)(configurationUtilities),
@ -148,6 +165,7 @@ export function getServiceNowITOMActionType(
id: ServiceNowITOMActionTypeId,
minimumLicenseRequired: 'platinum',
name: i18n.SERVICENOW_ITOM,
supportedFeatureIds: [AlertingConnectorFeatureId, SecurityConnectorFeatureId],
validate: {
config: schema.object(ExternalIncidentServiceConfigurationBase, {
validate: curry(validate.config)(configurationUtilities),

View file

@ -26,6 +26,11 @@ import {
} from '../types';
import { ActionsConfigurationUtilities } from '../actions_config';
import { getCustomAgents } from './lib/get_custom_agents';
import {
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../common';
export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>;
export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions<
@ -70,6 +75,11 @@ export function getActionType({
name: i18n.translate('xpack.actions.builtin.slackTitle', {
defaultMessage: 'Slack',
}),
supportedFeatureIds: [
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
secrets: schema.object(secretsSchemaProps, {
validate: curry(validateActionTypeConfig)(configurationUtilities),

View file

@ -26,7 +26,11 @@ import {
} from './schema';
import { createExternalService } from './service';
import { api } from './api';
import {
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../../common';
interface GetActionTypeParams {
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
@ -51,6 +55,11 @@ export function getActionType(
name: i18n.translate('xpack.actions.builtin.swimlaneTitle', {
defaultMessage: 'Swimlane',
}),
supportedFeatureIds: [
AlertingConnectorFeatureId,
CasesConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: schema.object(SwimlaneServiceConfiguration, {
validate: curry(validate.config)(configurationUtilities),

View file

@ -18,6 +18,11 @@ 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';
import {
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../common';
export type TeamsActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>;
export type TeamsActionTypeExecutorOptions = ActionTypeExecutorOptions<
@ -58,6 +63,11 @@ export function getActionType({
name: i18n.translate('xpack.actions.builtin.teamsTitle', {
defaultMessage: 'Microsoft Teams',
}),
supportedFeatureIds: [
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
secrets: schema.object(secretsSchemaProps, {
validate: curry(validateActionTypeConfig)(configurationUtilities),

View file

@ -19,6 +19,11 @@ import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from
import { ActionsConfigurationUtilities } from '../actions_config';
import { request } from './lib/axios_utils';
import { renderMustacheString } from '../lib/mustache_renderer';
import {
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '../../common';
// config definition
export enum WebhookMethods {
@ -88,6 +93,11 @@ export function getActionType({
name: i18n.translate('xpack.actions.builtin.webhookTitle', {
defaultMessage: 'Webhook',
}),
supportedFeatureIds: [
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
],
validate: {
config: schema.object(configSchemaProps, {
validate: curry(validateActionTypeConfig)(configurationUtilities),

View file

@ -12,6 +12,7 @@ import { Logger } from '@kbn/core/server';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { ActionsConfigurationUtilities } from '../actions_config';
import { postXmatters } from './lib/post_xmatters';
import { AlertingConnectorFeatureId } from '../../common';
export type XmattersActionType = ActionType<
ActionTypeConfigType,
@ -68,6 +69,7 @@ export function getActionType({
name: i18n.translate('xpack.actions.builtin.xmattersTitle', {
defaultMessage: 'xMatters',
}),
supportedFeatureIds: [AlertingConnectorFeatureId],
validate: {
config: schema.object(configSchemaProps, {
validate: curry(validateActionTypeConfig)(configurationUtilities),

View file

@ -70,6 +70,7 @@ describe('findAndCleanupTasks', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
jest.requireMock('./cleanup_tasks').cleanupTasks.mockResolvedValue({

View file

@ -59,6 +59,7 @@ test('successfully executes', async () => {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};
const actionSavedObject = {
@ -183,6 +184,7 @@ test('successfully executes as a task', async () => {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};
const actionSavedObject = {
@ -233,6 +235,7 @@ test('provides empty config when config and / or secrets is empty', async () =>
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};
const actionSavedObject = {
@ -265,6 +268,7 @@ test('throws an error when config is invalid', async () => {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
validate: {
config: schema.object({
param1: schema.string(),
@ -305,6 +309,7 @@ test('throws an error when connector is invalid', async () => {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
validate: {
connector: () => {
return 'error';
@ -345,6 +350,7 @@ test('throws an error when params is invalid', async () => {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
validate: {
params: schema.object({
param1: schema.string(),
@ -392,6 +398,7 @@ test('throws an error if actionType is not enabled', async () => {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};
const actionSavedObject = {
@ -427,6 +434,7 @@ test('should not throws an error if actionType is preconfigured', async () => {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};
const actionSavedObject = {
@ -745,6 +753,7 @@ function setupActionExecutorMock() {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};
const actionSavedObject = {

View file

@ -12,6 +12,7 @@ const sampleActionType: ActionType = {
id: 'test',
name: 'test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
async executor({ actionId }) {
return { status: 'ok', actionId };
},

View file

@ -61,6 +61,7 @@ describe('isLicenseValidForActionType', () => {
id: 'foo',
name: 'Foo',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
executor: async (options) => {
return { status: 'ok', actionId: options.actionId };
},
@ -156,6 +157,7 @@ describe('ensureLicenseForActionType()', () => {
id: 'foo',
name: 'Foo',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
executor: async (options) => {
return { status: 'ok', actionId: options.actionId };
},

View file

@ -24,6 +24,7 @@ test('should validate when there are no validators', () => {
id: 'foo',
name: 'bar',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
};
const testValue = { any: ['old', 'thing'] };
@ -37,6 +38,7 @@ test('should validate when there are no individual validators', () => {
id: 'foo',
name: 'bar',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
validate: {},
};
@ -63,6 +65,7 @@ test('should validate when validators return incoming value', () => {
id: 'foo',
name: 'bar',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
validate: {
params: selfValidator,
@ -95,6 +98,7 @@ test('should validate when validators return different values', () => {
id: 'foo',
name: 'bar',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
validate: {
params: selfValidator,
@ -130,6 +134,7 @@ test('should throw with expected error when validators fail', () => {
id: 'foo',
name: 'bar',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
validate: {
params: erroringValidator,
@ -164,6 +169,7 @@ test('should work with @kbn/config-schema', () => {
id: 'foo',
name: 'bar',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
validate: {
params: testSchema,
@ -188,6 +194,7 @@ describe('validateConnectors', () => {
id: 'foo',
name: 'bar',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
executor,
validate: {
params: selfValidator,

View file

@ -145,6 +145,7 @@ describe('Actions Plugin', () => {
id: 'test',
name: 'test',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
async executor(options) {
return { status: 'ok', actionId: options.actionId };
},
@ -431,6 +432,7 @@ describe('Actions Plugin', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};
@ -453,6 +455,7 @@ describe('Actions Plugin', () => {
id: 'my-action-type',
name: 'My action type',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
executor: jest.fn(),
};

View file

@ -41,6 +41,7 @@ describe('connectorTypesRoute', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'gold' as LicenseType,
supportedFeatureIds: ['alerting'],
},
];
@ -58,6 +59,9 @@ describe('connectorTypesRoute', () => {
"id": "1",
"minimum_license_required": "gold",
"name": "name",
"supported_feature_ids": Array [
"alerting",
],
},
],
}
@ -71,6 +75,81 @@ describe('connectorTypesRoute', () => {
enabled: true,
enabled_in_config: true,
enabled_in_license: true,
supported_feature_ids: ['alerting'],
minimum_license_required: 'gold',
},
],
});
});
it('passes feature_id if provided as query parameter', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
connectorTypesRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector_types"`);
const listTypes = [
{
id: '1',
name: 'name',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
minimumLicenseRequired: 'gold' as LicenseType,
},
];
const actionsClient = actionsClientMock.create();
actionsClient.listTypes.mockResolvedValueOnce(listTypes);
const [context, req, res] = mockHandlerArguments(
{ actionsClient },
{
query: {
feature_id: 'alerting',
},
},
['ok']
);
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Array [
Object {
"enabled": true,
"enabled_in_config": true,
"enabled_in_license": true,
"id": "1",
"minimum_license_required": "gold",
"name": "name",
"supported_feature_ids": Array [
"alerting",
],
},
],
}
`);
expect(actionsClient.listTypes).toHaveBeenCalledTimes(1);
expect(actionsClient.listTypes.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alerting",
]
`);
expect(res.ok).toHaveBeenCalledWith({
body: [
{
id: '1',
name: 'name',
enabled: true,
enabled_in_config: true,
enabled_in_license: true,
supported_feature_ids: ['alerting'],
minimum_license_required: 'gold',
},
],
@ -94,6 +173,7 @@ describe('connectorTypesRoute', () => {
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
minimumLicenseRequired: 'gold' as LicenseType,
},
];
@ -135,6 +215,7 @@ describe('connectorTypesRoute', () => {
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
minimumLicenseRequired: 'gold' as LicenseType,
},
];

View file

@ -6,18 +6,32 @@
*/
import { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { ILicenseState } from '../lib';
import { ActionType, BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common';
import { ActionsRequestHandlerContext } from '../types';
import { verifyAccessAndContext } from './verify_access_and_context';
const querySchema = schema.object({
feature_id: schema.maybe(schema.string()),
});
const rewriteBodyRes: RewriteResponseCase<ActionType[]> = (results) => {
return results.map(({ enabledInConfig, enabledInLicense, minimumLicenseRequired, ...res }) => ({
...res,
enabled_in_config: enabledInConfig,
enabled_in_license: enabledInLicense,
minimum_license_required: minimumLicenseRequired,
}));
return results.map(
({
enabledInConfig,
enabledInLicense,
minimumLicenseRequired,
supportedFeatureIds,
...res
}) => ({
...res,
enabled_in_config: enabledInConfig,
enabled_in_license: enabledInLicense,
minimum_license_required: minimumLicenseRequired,
supported_feature_ids: supportedFeatureIds,
})
);
};
export const connectorTypesRoute = (
@ -27,13 +41,15 @@ export const connectorTypesRoute = (
router.get(
{
path: `${BASE_ACTION_API_PATH}/connector_types`,
validate: {},
validate: {
query: querySchema,
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const actionsClient = (await context.actions).getActionsClient();
return res.ok({
body: rewriteBodyRes(await actionsClient.listTypes()),
body: rewriteBodyRes(await actionsClient.listTypes(req.query?.feature_id)),
});
})
)

View file

@ -49,6 +49,7 @@ describe('listActionTypesRoute', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'gold' as LicenseType,
supportedFeatureIds: ['alerting'],
},
];
@ -66,6 +67,9 @@ describe('listActionTypesRoute', () => {
"id": "1",
"minimumLicenseRequired": "gold",
"name": "name",
"supportedFeatureIds": Array [
"alerting",
],
},
],
}
@ -94,6 +98,7 @@ describe('listActionTypesRoute', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'gold' as LicenseType,
supportedFeatureIds: ['alerting'],
},
];
@ -135,6 +140,7 @@ describe('listActionTypesRoute', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'gold' as LicenseType,
supportedFeatureIds: ['alerting'],
},
];

View file

@ -35,6 +35,7 @@ describe('Executor', () => {
id: '.test',
name: 'Test',
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting'],
schema: {
config: TestConfigSchema,
secrets: TestSecretsSchema,

View file

@ -22,6 +22,7 @@ describe('Registration', () => {
id: '.test',
name: 'Test',
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting'],
schema: {
config: TestConfigSchema,
secrets: TestSecretsSchema,
@ -51,6 +52,7 @@ describe('Registration', () => {
id: connector.id,
name: connector.name,
minimumLicenseRequired: connector.minimumLicenseRequired,
supportedFeatureIds: connector.supportedFeatureIds,
validate: expect.anything(),
executor: expect.anything(),
});

View file

@ -51,6 +51,7 @@ export const register = <Config extends ActionTypeConfig, Secrets extends Action
id: connector.id,
name: connector.name,
minimumLicenseRequired: connector.minimumLicenseRequired,
supportedFeatureIds: connector.supportedFeatureIds,
validate: validators,
executor,
});

View file

@ -38,6 +38,7 @@ export interface SubActionConnectorType<Config, Secrets> {
id: string;
name: string;
minimumLicenseRequired: LicenseType;
supportedFeatureIds: string[];
schema: {
config: Type<Config>;
secrets: Type<Secrets>;

View file

@ -25,6 +25,7 @@ describe('Validators', () => {
id: '.test',
name: 'Test',
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting'],
schema: {
config: TestConfigSchema,
secrets: TestSecretsSchema,

View file

@ -113,6 +113,7 @@ export interface ActionType<
name: string;
maxAttempts?: number;
minimumLicenseRequired: LicenseType;
supportedFeatureIds: string[];
validate?: {
params?: ValidatorType<Params>;
config?: ValidatorType<Config>;

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ConnectorTypes } from './api';
import { CasesFeaturesAllRequired } from './ui/types';
export const DEFAULT_DATE_FORMAT = 'dateFormat' as const;
@ -88,14 +87,6 @@ export const ACTION_URL = '/api/actions' as const;
export const ACTION_TYPES_URL = `${ACTION_URL}/connector_types` as const;
export const CONNECTORS_URL = `${ACTION_URL}/connectors` as const;
export const SUPPORTED_CONNECTORS = [
`${ConnectorTypes.serviceNowITSM}`,
`${ConnectorTypes.serviceNowSIR}`,
`${ConnectorTypes.jira}`,
`${ConnectorTypes.resilient}`,
`${ConnectorTypes.swimlane}`,
];
/**
* Alerts
*/

View file

@ -70,6 +70,7 @@ export const actionTypesMock: ActionTypeConnector[] = [
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
},
{
id: '.index',
@ -78,6 +79,7 @@ export const actionTypesMock: ActionTypeConnector[] = [
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
},
{
id: '.servicenow',
@ -86,6 +88,7 @@ export const actionTypesMock: ActionTypeConnector[] = [
enabled: false,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting', 'cases'],
},
{
id: '.jira',
@ -94,6 +97,7 @@ export const actionTypesMock: ActionTypeConnector[] = [
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting', 'cases'],
},
{
id: '.resilient',
@ -102,6 +106,7 @@ export const actionTypesMock: ActionTypeConnector[] = [
enabled: false,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting', 'cases'],
},
{
id: '.servicenow-sir',
@ -110,5 +115,6 @@ export const actionTypesMock: ActionTypeConnector[] = [
enabled: false,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting', 'cases'],
},
];

View file

@ -553,20 +553,7 @@ describe('ConfigureCases', () => {
expect(wrapper.find('[data-test-subj="add-connector-flyout"]').exists()).toBe(true);
expect(getAddConnectorFlyoutMock).toHaveBeenCalledWith(
expect.objectContaining({
supportedActionTypes: [
expect.objectContaining({
id: '.servicenow',
}),
expect.objectContaining({
id: '.jira',
}),
expect.objectContaining({
id: '.resilient',
}),
expect.objectContaining({
id: '.servicenow-sir',
}),
],
featureId: 'cases',
})
);
});

View file

@ -13,7 +13,7 @@ import { EuiCallOut, EuiLink } from '@elastic/eui';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ActionConnectorTableItem } from '@kbn/triggers-actions-ui-plugin/public/types';
import { SUPPORTED_CONNECTORS } from '../../../common/constants';
import { CasesConnectorFeatureId } from '@kbn/actions-plugin/common';
import { useKibana } from '../../common/lib/kibana';
import { useGetActionTypes } from '../../containers/configure/use_action_types';
import { useCaseConfigure } from '../../containers/configure/use_configure';
@ -85,11 +85,6 @@ export const ConfigureCases: React.FC = React.memo(() => {
refetch: refetchActionTypes,
} = useGetActionTypes();
const supportedActionTypes = useMemo(
() => actionTypes.filter((actionType) => SUPPORTED_CONNECTORS.includes(actionType.id)),
[actionTypes]
);
const onConnectorUpdated = useCallback(async () => {
refetchConnectors();
refetchActionTypes();
@ -169,12 +164,12 @@ export const ConfigureCases: React.FC = React.memo(() => {
addFlyoutVisible
? triggersActionsUi.getAddConnectorFlyout({
onClose: onCloseAddFlyout,
supportedActionTypes,
featureId: CasesConnectorFeatureId,
onConnectorCreated: onConnectorUpdated,
})
: null,
// eslint-disable-next-line react-hooks/exhaustive-deps
[addFlyoutVisible, supportedActionTypes]
[addFlyoutVisible]
);
const ConnectorEditFlyout = useMemo(

View file

@ -152,6 +152,9 @@ describe('Case Configuration API', () => {
expect(fetchMock).toHaveBeenCalledWith('/api/actions/connector_types', {
method: 'GET',
signal: abortCtrl.signal,
query: {
feature_id: 'cases',
},
});
});

View file

@ -6,6 +6,7 @@
*/
import { isEmpty } from 'lodash/fp';
import { CasesConnectorFeatureId } from '@kbn/actions-plugin/common';
import { getAllConnectorTypesUrl } from '../../../common/utils/connectors_api';
import {
ActionConnector,
@ -93,7 +94,7 @@ export const patchCaseConfigure = async (
export const fetchActionTypes = async ({ signal }: ApiProps): Promise<ActionTypeConnector[]> => {
const response = await KibanaServices.get().http.fetch<ActionTypeConnector[]>(
getAllConnectorTypesUrl(),
{ method: 'GET', signal }
{ method: 'GET', signal, query: { feature_id: CasesConnectorFeatureId } }
);
return convertArrayToCamelCase(response) as ActionTypeConnector[];

View file

@ -25,6 +25,7 @@ describe('client', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'cases'],
},
{
id: '.servicenow',
@ -33,6 +34,7 @@ describe('client', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'cases'],
},
{
id: '.unsupported',
@ -41,6 +43,7 @@ describe('client', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting'],
},
{
id: '.swimlane',
@ -49,6 +52,7 @@ describe('client', () => {
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'cases'],
},
];

View file

@ -14,7 +14,7 @@ import { identity } from 'fp-ts/lib/function';
import { SavedObject, SavedObjectsFindResponse, SavedObjectsUtils } from '@kbn/core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { FindActionResult } from '@kbn/actions-plugin/server/types';
import { ActionType } from '@kbn/actions-plugin/common';
import { ActionType, CasesConnectorFeatureId } from '@kbn/actions-plugin/common';
import {
CaseConfigurationsResponseRt,
CaseConfigureResponseRt,
@ -31,7 +31,7 @@ import {
GetConfigureFindRequestRt,
throwErrors,
} from '../../../common/api';
import { MAX_CONCURRENT_SEARCHES, SUPPORTED_CONNECTORS } from '../../../common/constants';
import { MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import { createCaseError } from '../../common/error';
import { CasesClientInternal } from '../client_internal';
import { CasesClientArgs } from '../types';
@ -222,8 +222,9 @@ function isConnectorSupported(
actionTypes: Record<string, ActionType>
): boolean {
return (
SUPPORTED_CONNECTORS.includes(action.actionTypeId) &&
actionTypes[action.actionTypeId]?.enabledInLicense
(actionTypes[action.actionTypeId]?.supportedFeatureIds ?? []).includes(
CasesConnectorFeatureId
) && actionTypes[action.actionTypeId]?.enabledInLicense
);
}

View file

@ -39,6 +39,9 @@ jest.mock('@kbn/triggers-actions-ui-plugin/public/application/lib/action_connect
loadAllActions: jest.fn(),
loadActionTypes: jest.fn(),
}));
const { loadActionTypes } = jest.requireMock(
'@kbn/triggers-actions-ui-plugin/public/application/lib/action_connector_api'
);
jest.mock('@kbn/triggers-actions-ui-plugin/public/application/lib/rule_api', () => ({
loadAlertTypes: jest.fn(),
@ -222,6 +225,18 @@ describe('alert_form', () => {
mutedInstanceIds: [],
} as unknown as Rule;
loadActionTypes.mockResolvedValue([
{
id: actionType.id,
name: 'Test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
const KibanaReactContext = createKibanaReactContext(Legacy.shims.kibanaServices);
const actionWrapper = mount(
@ -238,16 +253,7 @@ describe('alert_form', () => {
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
}
actionTypeRegistry={actionTypeRegistry}
actionTypes={[
{
id: actionType.id,
name: 'Test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
]}
featureId="alerting"
/>
</KibanaReactContext.Provider>
</I18nProvider>
@ -265,7 +271,7 @@ describe('alert_form', () => {
it('renders available action cards', async () => {
const wrapperTwo = await setup();
const actionOption = wrapperTwo.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
`[data-test-subj="${actionType.id}-alerting-ActionTypeSelectOption"]`
);
expect(actionOption.exists()).toBeTruthy();
});

View file

@ -330,24 +330,6 @@ export const ML_GROUP_ID = 'security' as const;
export const LEGACY_ML_GROUP_ID = 'siem' as const;
export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID] as const;
/*
Rule notifications options
*/
export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [
'.email',
'.index',
'.jira',
'.pagerduty',
'.resilient',
'.servicenow',
'.servicenow-sir',
'.servicenow-itom',
'.slack',
'.swimlane',
'.teams',
'.webhook',
];
export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions' as const;
export const NOTIFICATION_THROTTLE_RULE = 'rule' as const;

View file

@ -16,7 +16,7 @@ export const ACTIONS_EDIT_TAB = '[data-test-subj="edit-rule-actions-tab"]';
export const ACTIONS_THROTTLE_INPUT =
'[data-test-subj="stepRuleActions"] [data-test-subj="select"]';
export const EMAIL_ACTION_BTN = '[data-test-subj=".email-ActionTypeSelectOption"]';
export const EMAIL_ACTION_BTN = '[data-test-subj=".email-siem-ActionTypeSelectOption"]';
export const CREATE_ACTION_CONNECTOR_BTN = '[data-test-subj="createActionConnectorButton-0"]';

View file

@ -8,11 +8,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import { getSupportedActions, RuleActionsField } from '.';
import { RuleActionsField } from '.';
import { useForm, Form } from '../../../../shared_imports';
import { useKibana } from '../../../../common/lib/kibana';
import { useFormFieldMock } from '../../../../common/mock';
import type { ActionType } from '@kbn/actions-plugin/common';
jest.mock('../../../../common/lib/kibana');
describe('RuleActionsField', () => {
@ -46,11 +45,7 @@ describe('RuleActionsField', () => {
return (
<Form form={form}>
<RuleActionsField
field={field}
messageVariables={messageVariables}
hasErrorOnCreationCaseAction={false}
/>
<RuleActionsField field={field} messageVariables={messageVariables} />
</Form>
);
};
@ -58,40 +53,4 @@ describe('RuleActionsField', () => {
expect(wrapper.dive().find('ActionForm')).toHaveLength(0);
});
describe('#getSupportedActions', () => {
const actions: ActionType[] = [
{
id: '.jira',
name: 'My Jira',
enabled: true,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
},
{
id: '.case',
name: 'Cases',
enabled: true,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
];
it('if we have an error on case action creation, we do not support case connector', () => {
expect(getSupportedActions(actions, true)).toMatchInlineSnapshot(`
Array [
Object {
"enabled": true,
"enabledInConfig": false,
"enabledInLicense": true,
"id": ".jira",
"minimumLicenseRequired": "gold",
"name": "My Jira",
},
]
`);
});
});
});

View file

@ -12,18 +12,16 @@ import deepMerge from 'deepmerge';
import ReactMarkdown from 'react-markdown';
import styled from 'styled-components';
import type { ActionType, ActionVariables } from '@kbn/triggers-actions-ui-plugin/public';
import { loadActionTypes } from '@kbn/triggers-actions-ui-plugin/public';
import type { ActionVariables } from '@kbn/triggers-actions-ui-plugin/public';
import type { RuleAction } from '@kbn/alerting-plugin/common';
import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../common/constants';
import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
import type { FieldHook } from '../../../../shared_imports';
import { useFormContext } from '../../../../shared_imports';
import { convertArrayToCamelCase, useKibana } from '../../../../common/lib/kibana';
import { useKibana } from '../../../../common/lib/kibana';
import { FORM_ERRORS_TITLE } from './translations';
interface Props {
field: FieldHook;
hasErrorOnCreationCaseAction: boolean;
messageVariables: ActionVariables;
}
@ -58,29 +56,11 @@ const ContainerActions = styled.div.attrs(
)}
`;
export const getSupportedActions = (
actionTypes: ActionType[],
hasErrorOnCreationCaseAction: boolean
): ActionType[] => {
return actionTypes.filter((actionType) => {
if (actionType.id === '.case' && hasErrorOnCreationCaseAction) {
return false;
}
return NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id);
});
};
export const RuleActionsField: React.FC<Props> = ({
field,
hasErrorOnCreationCaseAction,
messageVariables,
}) => {
export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) => {
const [fieldErrors, setFieldErrors] = useState<string | null>(null);
const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>();
const form = useFormContext();
const { isSubmitted, isSubmitting, isValid } = form;
const {
http,
triggersActionsUi: { getActionForm },
} = useKibana().services;
@ -141,7 +121,7 @@ export const RuleActionsField: React.FC<Props> = ({
setActionIdByIndex,
setActions: setAlertActionsProperty,
setActionParamsProperty,
actionTypes: supportedActionTypes,
featureId: SecurityConnectorFeatureId,
defaultActionMessage: DEFAULT_ACTION_MESSAGE,
}),
[
@ -151,19 +131,9 @@ export const RuleActionsField: React.FC<Props> = ({
setActionIdByIndex,
setActionParamsProperty,
setAlertActionsProperty,
supportedActionTypes,
]
);
useEffect(() => {
(async function () {
const actionTypes = convertArrayToCamelCase(await loadActionTypes({ http })) as ActionType[];
const supportedTypes = getSupportedActions(actionTypes, hasErrorOnCreationCaseAction);
setSupportedActionTypes(supportedTypes);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasErrorOnCreationCaseAction]);
useEffect(() => {
if (isSubmitting || !field.errors.length) {
return setFieldErrors(null);
@ -174,8 +144,6 @@ export const RuleActionsField: React.FC<Props> = ({
}
}, [isSubmitted, isSubmitting, field.isChangingValue, isValid, field.errors, setFieldErrors]);
if (!supportedActionTypes) return <></>;
return (
<ContainerActions $caseIndexes={caseActionIndexes}>
{fieldErrors ? (

View file

@ -69,7 +69,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
setForm,
actionMessageParams,
}) => {
const [isLoadingCaseAction, hasErrorOnCreationCaseAction] = useManageCaseAction();
const [isLoadingCaseAction] = useManageCaseAction();
const {
services: {
application,
@ -159,14 +159,13 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
component={RuleActionsField}
componentProps={{
messageVariables: actionMessageParams,
hasErrorOnCreationCaseAction,
}}
/>
</>
) : (
<UseField path="actions" component={GhostFormField} />
),
[throttle, actionMessageParams, hasErrorOnCreationCaseAction]
[throttle, actionMessageParams]
);
// only display the actions dropdown if the user has "read" privileges for actions
const displayActionsDropDown = useMemo(() => {

View file

@ -11,11 +11,7 @@ import { useDispatch } from 'react-redux';
import { EuiButtonEmpty } from '@elastic/eui';
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useFetcher } from '@kbn/observability-plugin/public';
import { getConnectorsAction } from '../../state/alerts/alerts';
import { fetchActionTypes } from '../../state/api/alerts';
import { ActionTypeId } from './types';
interface Props {
focusInput: () => void;
@ -26,18 +22,6 @@ interface KibanaDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export const ALLOWED_ACTION_TYPES: ActionTypeId[] = [
'.slack',
'.pagerduty',
'.server-log',
'.index',
'.teams',
'.servicenow',
'.jira',
'.webhook',
'.email',
];
export const AddConnectorFlyout = ({ focusInput, isDisabled }: Props) => {
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
const {
@ -51,8 +35,6 @@ export const AddConnectorFlyout = ({ focusInput, isDisabled }: Props) => {
const dispatch = useDispatch();
const { data: actionTypes } = useFetcher(() => fetchActionTypes(), []);
const ConnectorAddFlyout = useMemo(
() =>
getAddConnectorFlyout({
@ -61,12 +43,10 @@ export const AddConnectorFlyout = ({ focusInput, isDisabled }: Props) => {
setAddFlyoutVisibility(false);
focusInput();
},
supportedActionTypes: (actionTypes ?? []).filter((actionType) =>
ALLOWED_ACTION_TYPES.includes(actionType.id as ActionTypeId)
),
featureId: 'uptime',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[actionTypes]
[]
);
return (

View file

@ -20,14 +20,15 @@ import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
import { useFetcher } from '@kbn/observability-plugin/public';
import { SettingsFormProps } from '../../pages/settings';
import { connectorsSelector } from '../../state/alerts/alerts';
import { AddConnectorFlyout, ALLOWED_ACTION_TYPES } from './add_connector_flyout';
import { AddConnectorFlyout } from './add_connector_flyout';
import { useGetUrlParams, useUrlParams } from '../../hooks';
import { alertFormI18n } from './translations';
import { useInitApp } from '../../hooks/use_init_app';
import { ActionTypeId } from './types';
import { DefaultEmail } from './default_email';
import { fetchActionTypes } from '../../state/api/alerts';
type ConnectorOption = EuiComboBoxOptionOption<string>;
@ -63,6 +64,8 @@ export const AlertDefaultsForm: React.FC<SettingsFormProps> = ({
const inputRef = useRef<HTMLInputElement | null>(null);
const { data: actionTypes } = useFetcher(() => fetchActionTypes(), []);
useInitApp();
useEffect(() => {
@ -92,7 +95,7 @@ export const AlertDefaultsForm: React.FC<SettingsFormProps> = ({
};
const options = (data ?? [])
.filter((action) => ALLOWED_ACTION_TYPES.includes(action.actionTypeId as ActionTypeId))
.filter((action) => (actionTypes ?? []).find((type) => type.id === action.actionTypeId))
.map((connectorAction) => ({
value: connectorAction.id,
label: connectorAction.name,

View file

@ -22,6 +22,7 @@ describe('settings', () => {
id: '.slack',
minimumLicenseRequired: 'gold',
name: 'Slack',
supportedFeatureIds: ['uptime'],
},
]);
});

View file

@ -152,20 +152,22 @@ export const disableAlertById = async ({ alertId }: { alertId: string }) => {
};
export const fetchActionTypes = async (): Promise<ActionType[]> => {
const response = (await apiService.get(API_URLS.CONNECTOR_TYPES)) as Array<
AsApiContract<ActionType>
>;
const response = (await apiService.get(API_URLS.CONNECTOR_TYPES, {
feature_id: 'uptime',
})) as Array<AsApiContract<ActionType>>;
return response.map<ActionType>(
({
enabled_in_config: enabledInConfig,
enabled_in_license: enabledInLicense,
minimum_license_required: minimumLicenseRequired,
supported_feature_ids: supportedFeatureIds,
...res
}: AsApiContract<ActionType>) => ({
...res,
enabledInConfig,
enabledInLicense,
minimumLicenseRequired,
supportedFeatureIds,
})
);
};

View file

@ -1346,64 +1346,58 @@ Then this dependencies will be used to embed Actions form or register your own a
2. Add Actions form to React component:
```
import React, { useCallback } from 'react';
import { ActionForm } from '../../../../../../../../../plugins/triggers_actions_ui/public';
import { RuleAction } from '../../../../../../../../../plugins/triggers_actions_ui/public/types';
import React, { useCallback } from 'react';
import { ActionForm } from '../../../../../../../../../plugins/triggers_actions_ui/public';
import { RuleAction } from '../../../../../../../../../plugins/triggers_actions_ui/public/types';
const ALOWED_BY_PLUGIN_ACTION_TYPES = [
{ id: '.email', name: 'Email', enabled: true },
{ id: '.index', name: 'Index', enabled: false },
{ id: '.example-action', name: 'Example Action', enabled: false },
];
export const ComponentWithActionsForm: () => {
const { http, triggersActionsUi, notifications } = useKibana().services;
const actionTypeRegistry = triggersActionsUi.actionTypeRegistry;
const initialAlert = ({
name: 'test',
params: {},
consumer: 'alerts',
alertTypeId: '.index-threshold',
schedule: {
interval: '1m',
export const ComponentWithActionsForm: () => {
const { http, triggersActionsUi, notifications } = useKibana().services;
const actionTypeRegistry = triggersActionsUi.actionTypeRegistry;
const initialAlert = ({
name: 'test',
params: {},
consumer: 'alerts',
alertTypeId: '.index-threshold',
schedule: {
interval: '1m',
},
actions: [
{
group: 'default',
id: 'test',
actionTypeId: '.index',
params: {
message: '',
},
actions: [
{
group: 'default',
id: 'test',
actionTypeId: '.index',
params: {
message: '',
},
},
],
tags: [],
muteAll: false,
enabled: false,
mutedInstanceIds: [],
} as unknown) as Alert;
},
],
tags: [],
muteAll: false,
enabled: false,
mutedInstanceIds: [],
} as unknown) as Alert;
return (
<ActionForm
actions={initialAlert.actions}
messageVariables={[ { name: 'testVar1', description: 'test var1' } ]}
defaultActionGroupId={'default'}
setActionIdByIndex={(id: string, index: number) => {
initialAlert.actions[index].id = id;
}}
setRuleProperty={(_updatedActions: RuleAction[]) => {}}
setActionParamsProperty={(key: string, value: any, index: number) =>
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
}
http={http}
actionTypeRegistry={actionTypeRegistry}
defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'}
actionTypes={ALOWED_BY_PLUGIN_ACTION_TYPES}
toastNotifications={notifications.toasts}
consumer={initialAlert.consumer}
/>
);
};
return (
<ActionForm
actions={initialAlert.actions}
messageVariables={[ { name: 'testVar1', description: 'test var1' } ]}
defaultActionGroupId={'default'}
setActionIdByIndex={(id: string, index: number) => {
initialAlert.actions[index].id = id;
}}
setRuleProperty={(_updatedActions: RuleAction[]) => {}}
setActionParamsProperty={(key: string, value: any, index: number) =>
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
}
http={http}
actionTypeRegistry={actionTypeRegistry}
defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'}
featureId="alerting"
toastNotifications={notifications.toasts}
consumer={initialAlert.consumer}
/>
);
};
```
ActionForm Props definition:
@ -1420,7 +1414,7 @@ interface ActionAccordionFormProps {
actionTypeRegistry: ActionTypeRegistryContract;
toastNotifications: ToastsSetup;
docLinks: DocLinksStart;
actionTypes?: ActionType[];
featureId: string;
messageVariables?: ActionVariable[];
defaultActionMessage?: string;
capabilities: ApplicationStart['capabilities'];
@ -1441,7 +1435,7 @@ interface ActionAccordionFormProps {
| actionTypeRegistry | Registry for action types. |
| toastNotifications | Toast messages Plugin Setup Contract. |
| docLinks | Documentation links Plugin Start Contract. |
| actionTypes | Optional property, which allows to define a list of available actions specific for a current plugin. |
| featureId | Property that filters which action types are loaded when the flyout is opened. Each action type configures the feature ids it is available in during [server side registration](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions#action-types). |
| messageVariables | Optional property, which allows to define a list of variables for action 'message' property. Set `useWithTripleBracesInTemplates` to true if you don't want the variable escaped when rendering. |
| defaultActionMessage | Optional property, which allows to define a message value for action with 'message' property. |
| capabilities | Kibana core's Capabilities ApplicationStart['capabilities']. |
@ -1485,18 +1479,18 @@ import { ActionsConnectorsContextProvider, CreateConnectorFlyout } from '../../.
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
const onClose = useCallback(() => setAddFlyoutVisibility(false), []);
// load required dependancied
// load required dependancies
const { http, triggersActionsUi, notifications, application, docLinks } = useKibana().services;
const connector = {
secrets: {},
id: 'test',
actionTypeId: '.index',
actionType: 'Index',
name: 'action-connector',
referencedByCount: 0,
config: {},
};
secrets: {},
id: 'test',
actionTypeId: '.index',
actionType: 'Index',
name: 'action-connector',
referencedByCount: 0,
config: {},
};
// UI control item for open flyout
<EuiButton
@ -1512,18 +1506,11 @@ const connector = {
</EuiButton>
// in render section of component
<CreateConnectorFlyout
actionTypeRegistry={triggersActionsUi.actionTypeRegistry}
onClose={onClose}
setAddFlyoutVisibility={setAddFlyoutVisibility}
supportedActionTypes={[
{
id: '.index',
enabled: true,
name: 'Index',
},
]}
/>
<CreateConnectorFlyout
actionTypeRegistry={triggersActionsUi.actionTypeRegistry}
onClose={onClose}
setAddFlyoutVisibility={setAddFlyoutVisibility}
/>
```
CreateConnectorFlyout Props definition:
@ -1531,7 +1518,7 @@ CreateConnectorFlyout Props definition:
export interface ConnectorAddFlyoutProps {
actionTypeRegistry: ActionTypeRegistryContract;
onClose: () => void;
supportedActionTypes?: ActionType[];
featureId?: string;
onConnectorCreated?: (connector: ActionConnector) => void;
onTestConnector?: (connector: ActionConnector) => void;
}
@ -1541,7 +1528,7 @@ export interface ConnectorAddFlyoutProps {
| -------------------- | ----------------------------------------------------------------------------------------------------------------- |
| actionTypeRegistry | The action type registry. |
| onClose | Called when closing the flyout |
| supportedActionTypes | Optional property, that allows to define only specific action types list which is available for a current plugin. |
| featureId | Optional property that filters which action types are loaded when the flyout is opened. Each action type configures the feature ids it is available in during [server side registration](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions#action-types). |
| onConnectorCreated | Optional property. Function to be called after the creation of the connector. |
| onTestConnector | Optional property. Function to be called when the user press the Save & Test button. |

View file

@ -14,7 +14,7 @@ const http = httpServiceMock.createStartContract();
beforeEach(() => jest.resetAllMocks());
describe('loadActionTypes', () => {
test('should call get types API', async () => {
test('should call list types API', async () => {
const apiResponseValue = [
{
id: 'test',
@ -22,6 +22,7 @@ describe('loadActionTypes', () => {
enabled: true,
enabled_in_config: true,
enabled_in_license: true,
supported_feature_ids: ['alerting'],
minimum_license_required: 'basic',
},
];
@ -34,6 +35,7 @@ describe('loadActionTypes', () => {
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
minimumLicenseRequired: 'basic',
},
];
@ -46,4 +48,44 @@ describe('loadActionTypes', () => {
]
`);
});
test('should call list types API with query parameter if specified', async () => {
const apiResponseValue = [
{
id: 'test',
name: 'Test',
enabled: true,
enabled_in_config: true,
enabled_in_license: true,
supported_feature_ids: ['alerting'],
minimum_license_required: 'basic',
},
];
http.get.mockResolvedValueOnce(apiResponseValue);
const resolvedValue: ActionType[] = [
{
id: 'test',
name: 'Test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
minimumLicenseRequired: 'basic',
},
];
const result = await loadActionTypes({ http, featureId: 'alerting' });
expect(result).toEqual(resolvedValue);
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/api/actions/connector_types",
Object {
"query": Object {
"feature_id": "alerting",
},
},
]
`);
});
});

View file

@ -18,17 +18,34 @@ const rewriteBodyReq: RewriteRequestCase<ActionType> = ({
enabled_in_config: enabledInConfig,
enabled_in_license: enabledInLicense,
minimum_license_required: minimumLicenseRequired,
supported_feature_ids: supportedFeatureIds,
...res
}: AsApiContract<ActionType>) => ({
enabledInConfig,
enabledInLicense,
minimumLicenseRequired,
supportedFeatureIds,
...res,
});
export async function loadActionTypes({ http }: { http: HttpSetup }): Promise<ActionType[]> {
const res = await http.get<Parameters<typeof rewriteResponseRes>[0]>(
`${BASE_ACTION_API_PATH}/connector_types`
);
export async function loadActionTypes({
http,
featureId,
}: {
http: HttpSetup;
featureId?: string;
}): Promise<ActionType[]> {
const res = featureId
? await http.get<Parameters<typeof rewriteResponseRes>[0]>(
`${BASE_ACTION_API_PATH}/connector_types`,
{
query: {
feature_id: featureId,
},
}
)
: await http.get<Parameters<typeof rewriteResponseRes>[0]>(
`${BASE_ACTION_API_PATH}/connector_types`
);
return rewriteResponseRes(res);
}

View file

@ -13,6 +13,7 @@ test('should sort enabled action types first', async () => {
{
id: '1',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'first',
enabled: true,
enabledInConfig: true,
@ -21,6 +22,7 @@ test('should sort enabled action types first', async () => {
{
id: '2',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
name: 'second',
enabled: false,
enabledInConfig: true,
@ -29,6 +31,7 @@ test('should sort enabled action types first', async () => {
{
id: '3',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'third',
enabled: true,
enabledInConfig: true,
@ -37,6 +40,7 @@ test('should sort enabled action types first', async () => {
{
id: '4',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'x-fourth',
enabled: true,
enabledInConfig: false,
@ -55,6 +59,7 @@ test('should sort by name when all enabled', async () => {
{
id: '1',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'third',
enabled: true,
enabledInConfig: true,
@ -63,6 +68,7 @@ test('should sort by name when all enabled', async () => {
{
id: '2',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'first',
enabled: true,
enabledInConfig: true,
@ -71,6 +77,7 @@ test('should sort by name when all enabled', async () => {
{
id: '3',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'second',
enabled: true,
enabledInConfig: true,
@ -79,6 +86,7 @@ test('should sort by name when all enabled', async () => {
{
id: '4',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'x-fourth',
enabled: true,
enabledInConfig: false,

View file

@ -24,6 +24,7 @@ describe('checkActionTypeEnabled', () => {
const actionType: ActionType = {
id: '1',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'my action',
enabled: true,
enabledInConfig: true,
@ -40,6 +41,7 @@ describe('checkActionTypeEnabled', () => {
const actionType: ActionType = {
id: '1',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'my action',
enabled: false,
enabledInConfig: true,
@ -74,6 +76,7 @@ describe('checkActionTypeEnabled', () => {
const actionType: ActionType = {
id: '1',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'my action',
enabled: false,
enabledInConfig: false,
@ -117,6 +120,7 @@ describe('checkActionFormActionTypeEnabled', () => {
const actionType: ActionType = {
id: '1',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'my action',
enabled: true,
enabledInConfig: false,
@ -135,6 +139,7 @@ describe('checkActionFormActionTypeEnabled', () => {
const actionType: ActionType = {
id: 'disabled-by-config',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
name: 'my action',
enabled: true,
enabledInConfig: false,

View file

@ -24,6 +24,8 @@ jest.mock('../../lib/action_connector_api', () => ({
loadAllActions: jest.fn(),
loadActionTypes: jest.fn(),
}));
const { loadActionTypes } = jest.requireMock('../../lib/action_connector_api');
const setHasActionsWithBrokenConnector = jest.fn();
describe('action_form', () => {
const mockedActionParamsFields = lazy(async () => ({
@ -234,6 +236,63 @@ describe('action_form', () => {
mutedInstanceIds: [],
} as unknown as Rule;
loadActionTypes.mockResolvedValue([
{
id: actionType.id,
name: 'Test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: '.index',
name: 'Index',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: 'preconfigured',
name: 'Preconfigured only',
enabled: true,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: 'disabled-by-config',
name: 'Disabled by config',
enabled: false,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
{
id: 'disabled-by-license',
name: 'Disabled by license',
enabled: false,
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
{
id: '.jira',
name: 'Disabled by action type',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
const defaultActionMessage = 'Alert [{{context.metadata.name}}] has exceeded the threshold';
const wrapper = mountWithIntl(
<ActionForm
@ -246,6 +305,7 @@ describe('action_form', () => {
state: [],
context: [{ name: 'contextVar', description: 'context var1' }],
}}
featureId="alerting"
defaultActionGroupId={'default'}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => {
const recoveryActionGroupId = customRecoveredActionGroup
@ -275,56 +335,6 @@ describe('action_form', () => {
}
actionTypeRegistry={actionTypeRegistry}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
actionTypes={[
{
id: actionType.id,
name: 'Test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
{
id: '.index',
name: 'Index',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
{
id: 'preconfigured',
name: 'Preconfigured only',
enabled: true,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
{
id: 'disabled-by-config',
name: 'Disabled by config',
enabled: false,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
},
{
id: 'disabled-by-license',
name: 'Disabled by license',
enabled: false,
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
},
{
id: '.jira',
name: 'Disabled by action type',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
]}
/>
);
@ -340,35 +350,42 @@ describe('action_form', () => {
it('renders available action cards', async () => {
const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
`[data-test-subj="${actionType.id}-alerting-ActionTypeSelectOption"]`
);
expect(actionOption.exists()).toBeTruthy();
expect(
wrapper
.find(`EuiToolTip [data-test-subj="${actionType.id}-ActionTypeSelectOption"]`)
.find(`EuiToolTip [data-test-subj="${actionType.id}-alerting-ActionTypeSelectOption"]`)
.exists()
).toBeFalsy();
expect(setHasActionsWithBrokenConnector).toHaveBeenLastCalledWith(false);
expect(loadActionTypes).toBeCalledWith(
expect.objectContaining({
featureId: 'alerting',
})
);
});
it('does not render action types disabled by config', async () => {
const wrapper = await setup();
const actionOption = wrapper.find(
'[data-test-subj="disabled-by-config-ActionTypeSelectOption"]'
'[data-test-subj="disabled-by-config-alerting-ActionTypeSelectOption"]'
);
expect(actionOption.exists()).toBeFalsy();
});
it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => {
const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
const actionOption = wrapper.find(
'[data-test-subj="preconfigured-alerting-ActionTypeSelectOption"]'
);
expect(actionOption.exists()).toBeTruthy();
});
it('renders available action groups for the selected action type', async () => {
const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
`[data-test-subj="${actionType.id}-alerting-ActionTypeSelectOption"]`
);
actionOption.first().simulate('click');
const actionGroupsSelect = wrapper.find(
@ -403,7 +420,7 @@ describe('action_form', () => {
},
},
]);
const actionOption = wrapper.find(`[data-test-subj=".jira-ActionTypeSelectOption"]`);
const actionOption = wrapper.find(`[data-test-subj=".jira-alerting-ActionTypeSelectOption"]`);
actionOption.first().simulate('click');
const actionGroupsSelect = wrapper.find(
`[data-test-subj="addNewActionConnectorActionGroup-1"]`
@ -440,7 +457,7 @@ describe('action_form', () => {
],
'iHaveRecovered'
);
const actionOption = wrapper.find(`[data-test-subj=".jira-ActionTypeSelectOption"]`);
const actionOption = wrapper.find(`[data-test-subj=".jira-alerting-ActionTypeSelectOption"]`);
actionOption.first().simulate('click');
const actionGroupsSelect = wrapper.find(
`[data-test-subj="addNewActionConnectorActionGroup-1"]`
@ -466,7 +483,7 @@ describe('action_form', () => {
it('renders available connectors for the selected action type', async () => {
const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
`[data-test-subj="${actionType.id}-alerting-ActionTypeSelectOption"]`
);
actionOption.first().simulate('click');
const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}-0"]`);
@ -507,7 +524,9 @@ describe('action_form', () => {
it('renders only preconfigured connectors for the selected preconfigured action type', async () => {
const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
const actionOption = wrapper.find(
'[data-test-subj="preconfigured-alerting-ActionTypeSelectOption"]'
);
actionOption.first().simulate('click');
const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured-1"]');
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
@ -528,7 +547,9 @@ describe('action_form', () => {
it('does not render "Add connector" button for preconfigured only action type', async () => {
const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
const actionOption = wrapper.find(
'[data-test-subj="preconfigured-alerting-ActionTypeSelectOption"]'
);
actionOption.first().simulate('click');
const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]');
const addNewConnectorButton = preconfigPannel.find(
@ -540,12 +561,12 @@ describe('action_form', () => {
it('renders action types disabled by license', async () => {
const wrapper = await setup();
const actionOption = wrapper.find(
'[data-test-subj="disabled-by-license-ActionTypeSelectOption"]'
'[data-test-subj="disabled-by-license-alerting-ActionTypeSelectOption"]'
);
expect(actionOption.exists()).toBeTruthy();
expect(
wrapper
.find('EuiToolTip [data-test-subj="disabled-by-license-ActionTypeSelectOption"]')
.find('EuiToolTip [data-test-subj="disabled-by-license-alerting-ActionTypeSelectOption"]')
.exists()
).toBeTruthy();
});

View file

@ -26,7 +26,6 @@ import {
RuleAction,
ActionTypeIndex,
ActionConnector,
ActionType,
ActionVariables,
ActionTypeRegistryContract,
} from '../../../types';
@ -35,7 +34,7 @@ import { ActionTypeForm } from './action_type_form';
import { AddConnectorInline } from './connector_add_inline';
import { actionTypeCompare } from '../../lib/action_type_compare';
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
import { useKibana } from '../../../common/lib/kibana';
import { DefaultActionParamsGetter } from '../../lib/get_defaults_for_action_params';
import { ConnectorAddModal } from '.';
@ -56,7 +55,7 @@ export interface ActionAccordionFormProps {
setActionGroupIdByIndex?: (group: string, index: number) => void;
setActions: (actions: RuleAction[]) => void;
setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void;
actionTypes?: ActionType[];
featureId: string;
messageVariables?: ActionVariables;
setHasActionsDisabled?: (value: boolean) => void;
setHasActionsWithBrokenConnector?: (value: boolean) => void;
@ -77,7 +76,7 @@ export const ActionForm = ({
setActionGroupIdByIndex,
setActions,
setActionParamsProperty,
actionTypes,
featureId,
messageVariables,
actionGroups,
defaultActionMessage,
@ -112,8 +111,8 @@ export const ActionForm = ({
(async () => {
try {
setIsLoadingActionTypes(true);
const registeredActionTypes = (actionTypes ?? (await loadActionTypes({ http }))).sort(
(a, b) => a.name.localeCompare(b.name)
const registeredActionTypes = (await loadActionTypes({ http, featureId })).sort((a, b) =>
a.name.localeCompare(b.name)
);
const index: ActionTypeIndex = {};
for (const actionTypeItem of registeredActionTypes) {
@ -232,11 +231,6 @@ export const ActionForm = ({
const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured);
actionTypeNodes = actionTypeRegistry
.list()
/**
* TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502.
* If actionTypes are set, hidden connectors are filtered out. Otherwise, they are not.
*/
.filter(({ id }) => actionTypes ?? !DEFAULT_HIDDEN_ACTION_TYPES.includes(id))
.filter((item) => actionTypesIndex[item.id])
.filter((item) => !!item.actionParamsFields)
.sort((a, b) =>
@ -260,7 +254,7 @@ export const ActionForm = ({
<EuiKeyPadMenuItem
key={index}
isDisabled={!checkEnabledResult.isEnabled}
data-test-subj={`${item.id}-ActionTypeSelectOption`}
data-test-subj={`${item.id}-${featureId}-ActionTypeSelectOption`}
label={actionTypesIndex[item.id].name}
onClick={() => addActionType(item)}
>

View file

@ -246,6 +246,7 @@ function getActionTypeForm(
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
'.server-log': {
id: '.server-log',
@ -254,6 +255,7 @@ function getActionTypeForm(
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
};

View file

@ -6,13 +6,21 @@
*/
import * as React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { act } from '@testing-library/react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { coreMock } from '@kbn/core/public/mocks';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ActionTypeMenu } from './action_type_menu';
import { GenericValidationResult } from '../../../types';
import { useKibana } from '../../../common/lib/kibana';
jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/action_connector_api', () => ({
...(jest.requireActual('../../lib/action_connector_api') as any),
loadActionTypes: jest.fn(),
}));
const { loadActionTypes } = jest.requireMock('../../lib/action_connector_api');
const actionTypeRegistry = actionTypeRegistryMock.create();
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
@ -34,7 +42,7 @@ describe('connector_add_flyout', () => {
};
});
it('renders action type menu with proper EuiCards for registered action types', () => {
it('renders action type menu with proper EuiCards for registered action types', async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
@ -47,28 +55,33 @@ describe('connector_add_flyout', () => {
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypes={[
{
id: actionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
]}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeTruthy();
});
it(`doesn't renders action types that are disabled via config`, () => {
it(`doesn't renders action types that are disabled via config`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
@ -81,28 +94,33 @@ describe('connector_add_flyout', () => {
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypes={[
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
},
]}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeFalsy();
});
it(`renders action types as disabled when disabled by license`, () => {
it(`renders action types as disabled when disabled by license`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
@ -115,23 +133,28 @@ describe('connector_add_flyout', () => {
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypes={[
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
},
]}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('EuiToolTip [data-test-subj="my-action-type-card"]').exists()).toBeTruthy();
});

View file

@ -10,7 +10,6 @@ import { EuiFlexItem, EuiCard, EuiIcon, EuiFlexGrid, EuiSpacer } from '@elastic/
import { i18n } from '@kbn/i18n';
import { EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
import { ActionType, ActionTypeIndex, ActionTypeRegistryContract } from '../../../types';
import { loadActionTypes } from '../../lib/action_connector_api';
import { actionTypeCompare } from '../../lib/action_type_compare';
@ -20,14 +19,14 @@ import { SectionLoading } from '../../components/section_loading';
interface Props {
onActionTypeChange: (actionType: ActionType) => void;
actionTypes?: ActionType[];
featureId?: string;
setHasActionsUpgradeableByTrial?: (value: boolean) => void;
actionTypeRegistry: ActionTypeRegistryContract;
}
export const ActionTypeMenu = ({
onActionTypeChange,
actionTypes,
featureId,
setHasActionsUpgradeableByTrial,
actionTypeRegistry,
}: Props) => {
@ -41,21 +40,10 @@ export const ActionTypeMenu = ({
useEffect(() => {
(async () => {
try {
/**
* Hidden action types will be hidden only on Alerts & Actions.
* actionTypes prop is not filtered. Thus, any consumer that provides it's own actionTypes
* can use the hidden action types. For example, Cases or Detections of Security Solution.
*
* TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502.
* */
let availableActionTypes = actionTypes;
if (!availableActionTypes) {
setLoadingActionTypes(true);
availableActionTypes = (await loadActionTypes({ http })).filter(
(actionType) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(actionType.id)
);
setLoadingActionTypes(false);
}
setLoadingActionTypes(true);
const availableActionTypes = await loadActionTypes({ http, featureId });
setLoadingActionTypes(false);
const index: ActionTypeIndex = {};
for (const actionTypeItem of availableActionTypes) {
index[actionTypeItem.id] = actionTypeItem;

View file

@ -57,6 +57,7 @@ describe('connector_add_modal', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
};
const wrapper = mountWithIntl(

View file

@ -55,6 +55,7 @@ describe('connectors_selection', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
};

View file

@ -7,6 +7,7 @@
import React, { memo } from 'react';
import {
EuiBadge,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
@ -14,14 +15,17 @@ import {
EuiText,
EuiFlyoutHeader,
IconType,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { getConnectorFeatureName } from '@kbn/actions-plugin/common';
const FlyoutHeaderComponent: React.FC<{
icon?: IconType | null;
actionTypeName?: string | null;
actionTypeMessage?: string | null;
}> = ({ icon, actionTypeName, actionTypeMessage }) => {
featureIds?: string[] | null;
}> = ({ icon, actionTypeName, actionTypeMessage, featureIds }) => {
return (
<EuiFlyoutHeader hasBorder data-test-subj="create-connector-flyout-header">
<EuiFlexGroup gutterSize="m" alignItems="center">
@ -47,6 +51,28 @@ const FlyoutHeaderComponent: React.FC<{
<EuiText size="s" color="subdued">
{actionTypeMessage}
</EuiText>
{featureIds && featureIds.length > 0 && (
<>
<EuiSpacer size="m" />
<EuiFlexGroup
data-test-subj="create-connector-flyout-header-availability"
wrap
responsive={false}
gutterSize="xs"
alignItems="center"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.addConnectorForm.flyoutHeaderAvailability"
defaultMessage="Availability:"
/>{' '}
{featureIds.map((featureId: string) => (
<EuiFlexItem grow={false} key={featureId}>
<EuiBadge color="default">{getConnectorFeatureName(featureId)}</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
</>
)}
</>
) : (
<EuiTitle size="s">

View file

@ -17,6 +17,13 @@ import {
} from '../../../components/builtin_action_types/test_utils';
import CreateConnectorFlyout from '.';
jest.mock('../../../lib/action_connector_api', () => ({
...(jest.requireActual('../../../lib/action_connector_api') as any),
loadActionTypes: jest.fn(),
}));
const { loadActionTypes } = jest.requireMock('../../../lib/action_connector_api');
const createConnectorResponse = {
connector_type_id: 'test',
is_preconfigured: false,
@ -37,7 +44,7 @@ describe('CreateConnectorFlyout', () => {
actionConnectorFields: lazy(() => import('../connector_mock')),
});
const actionTypes = [
loadActionTypes.mockResolvedValue([
{
id: actionTypeModel.id,
enabled: true,
@ -45,8 +52,9 @@ describe('CreateConnectorFlyout', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'siem'],
},
];
]);
const actionTypeRegistry = actionTypeRegistryMock.create();
@ -67,7 +75,6 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
@ -78,96 +85,130 @@ describe('CreateConnectorFlyout', () => {
expect(getByTestId('create-connector-flyout-footer')).toBeInTheDocument();
});
it('renders action type menu on flyout open', () => {
it('renders action type menu on flyout open', async () => {
const { getByTestId } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
expect(getByTestId(`${actionTypeModel.id}-card`)).toBeInTheDocument();
await act(() => Promise.resolve());
expect(await getByTestId(`${actionTypeModel.id}-card`)).toBeInTheDocument();
});
describe('Licensing', () => {
it('renders banner with subscription links when gold features are disabled due to licensing', () => {
it('renders banner with subscription links when gold features are disabled due to licensing', async () => {
const disabledActionType = actionTypeRegistryMock.createMockActionTypeModel();
loadActionTypes.mockResolvedValueOnce([
{
id: actionTypeModel.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'siem'],
},
{
id: disabledActionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
const { getByTestId } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={[
...actionTypes,
{
id: disabledActionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
},
]}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(getByTestId('upgrade-your-license-callout')).toBeInTheDocument();
});
it('does not render banner with subscription links when only platinum features are disabled due to licensing', () => {
it('does not render banner with subscription links when only platinum features are disabled due to licensing', async () => {
const disabledActionType = actionTypeRegistryMock.createMockActionTypeModel();
loadActionTypes.mockResolvedValueOnce([
{
id: actionTypeModel.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'siem'],
},
{
id: disabledActionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
supportedFeatureIds: ['alerting'],
minimumLicenseRequired: 'platinum',
},
]);
const { queryByTestId } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={[
...actionTypes,
{
id: disabledActionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'platinum',
},
]}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(queryByTestId('upgrade-your-license-callout')).toBeFalsy();
});
it('does not render banner with subscription links when only enterprise features are disabled due to licensing', () => {
it('does not render banner with subscription links when only enterprise features are disabled due to licensing', async () => {
const disabledActionType = actionTypeRegistryMock.createMockActionTypeModel();
loadActionTypes.mockResolvedValueOnce([
{
id: actionTypeModel.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'siem'],
},
{
id: disabledActionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'enterprise',
supportedFeatureIds: ['alerting'],
},
]);
const { queryByTestId } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={[
...actionTypes,
{
id: disabledActionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'enterprise',
},
]}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(queryByTestId('upgrade-your-license-callout')).toBeFalsy();
});
});
@ -178,12 +219,13 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(queryByTestId('create-connector-flyout-header-icon')).not.toBeInTheDocument();
});
@ -192,26 +234,53 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(getByText('Select a connector')).toBeInTheDocument();
});
it('shows the feature id badges when the connector type is selected', async () => {
const { getByTestId, getByText } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId(`${actionTypeModel.id}-card`));
});
await waitFor(() => {
expect(getByTestId('test-connector-text-field')).toBeInTheDocument();
});
expect(getByTestId('create-connector-flyout-header-availability')).toBeInTheDocument();
expect(getByText('Alerting')).toBeInTheDocument();
expect(getByText('Security Solution')).toBeInTheDocument();
});
it('shows the icon when the connector type is selected', async () => {
const { getByTestId, getByText, queryByText } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId(`${actionTypeModel.id}-card`));
});
@ -233,11 +302,11 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId(`${actionTypeModel.id}-card`));
@ -286,24 +355,26 @@ describe('CreateConnectorFlyout', () => {
});
actionTypeRegistry.get.mockReturnValue(errorActionTypeModel);
loadActionTypes.mockResolvedValueOnce([
{
id: errorActionTypeModel.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
},
]);
const { getByTestId, getByText } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={[
{
id: errorActionTypeModel.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
},
]}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId(`${errorActionTypeModel.id}-card`));
@ -338,11 +409,11 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId(`${actionTypeModel.id}-card`));
@ -401,11 +472,11 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(getByTestId('create-connector-flyout-cancel-btn')).toBeInTheDocument();
expect(getByTestId('create-connector-flyout-save-test-btn')).toBeInTheDocument();
@ -417,11 +488,11 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId(`${actionTypeModel.id}-card`));
@ -437,11 +508,11 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId(`${actionTypeModel.id}-card`));
@ -456,6 +527,8 @@ describe('CreateConnectorFlyout', () => {
userEvent.click(getByTestId('create-connector-flyout-back-btn'));
});
await act(() => Promise.resolve());
expect(getByTestId(`${actionTypeModel.id}-card`)).toBeInTheDocument();
});
@ -464,10 +537,10 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
/>
);
await act(() => Promise.resolve());
expect(queryByTestId('create-connector-flyout-save-test-btn')).not.toBeInTheDocument();
});
@ -477,11 +550,11 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId('create-connector-flyout-cancel-btn'));
@ -500,11 +573,11 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(getByTestId('create-connector-flyout-cancel-btn')).not.toBeDisabled();
expect(getByTestId('create-connector-flyout-save-test-btn')).toBeDisabled();
@ -516,11 +589,11 @@ describe('CreateConnectorFlyout', () => {
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
supportedActionTypes={actionTypes}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
act(() => {
userEvent.click(getByTestId(`${actionTypeModel.id}-card`));

View file

@ -21,20 +21,20 @@ import { useCreateConnector } from '../../../hooks/use_create_connector';
import { ConnectorForm, ConnectorFormState } from '../connector_form';
import { ConnectorFormSchema } from '../types';
import { FlyoutHeader } from './header';
import { FlyoutFooter } from './foooter';
import { FlyoutFooter } from './footer';
import { UpgradeLicenseCallOut } from './upgrade_license_callout';
export interface CreateConnectorFlyoutProps {
actionTypeRegistry: ActionTypeRegistryContract;
onClose: () => void;
supportedActionTypes?: ActionType[];
featureId?: string;
onConnectorCreated?: (connector: ActionConnector) => void;
onTestConnector?: (connector: ActionConnector) => void;
}
const CreateConnectorFlyoutComponent: React.FC<CreateConnectorFlyoutProps> = ({
actionTypeRegistry,
supportedActionTypes,
featureId,
onClose,
onConnectorCreated,
onTestConnector,
@ -156,13 +156,14 @@ const CreateConnectorFlyoutComponent: React.FC<CreateConnectorFlyoutProps> = ({
icon={actionTypeModel?.iconClass}
actionTypeName={actionType?.name}
actionTypeMessage={actionTypeModel?.selectMessage}
featureIds={actionType?.supportedFeatureIds}
/>
<EuiFlyoutBody
banner={!actionType && hasActionsUpgradeableByTrial ? <UpgradeLicenseCallOut /> : null}
>
{actionType == null ? (
<ActionTypeMenu
actionTypes={supportedActionTypes}
featureId={featureId}
onActionTypeChange={setActionType}
setHasActionsUpgradeableByTrial={setHasActionsUpgradeableByTrial}
actionTypeRegistry={actionTypeRegistry}

View file

@ -26,7 +26,7 @@ import { hasSaveActionsCapability } from '../../../lib/capabilities';
import TestConnectorForm from '../test_connector_form';
import { useExecuteConnector } from '../../../hooks/use_execute_connector';
import { FlyoutHeader } from './header';
import { FlyoutFooter } from './foooter';
import { FlyoutFooter } from './footer';
export interface EditConnectorFlyoutProps {
actionTypeRegistry: ActionTypeRegistryContract;

View file

@ -6,6 +6,7 @@
*/
import * as React from 'react';
import { uniq } from 'lodash';
// eslint-disable-next-line @kbn/eslint/module_migration
import { ThemeProvider } from 'styled-components';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
@ -39,10 +40,12 @@ describe('actions_connectors_list component empty', () => {
{
id: 'test',
name: 'Test',
supportedFeatureIds: ['alerting'],
},
{
id: 'test2',
name: 'Test2',
supportedFeatureIds: ['alerting'],
},
]);
actionTypeRegistry.has.mockReturnValue(true);
@ -140,11 +143,13 @@ describe('actions_connectors_list component with items', () => {
id: 'test',
name: 'Test',
enabled: true,
supportedFeatureIds: ['alerting'],
},
{
id: 'test2',
name: 'Test2',
enabled: true,
supportedFeatureIds: ['alerting', 'cases'],
},
]);
@ -197,6 +202,13 @@ describe('actions_connectors_list component with items', () => {
await setup();
expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1);
expect(wrapper.find('EuiTableRow')).toHaveLength(4);
const featureIdsBadges = wrapper.find(
'EuiBadge[data-test-subj="connectorsTableCell-featureIds"]'
);
expect(featureIdsBadges).toHaveLength(5);
expect(uniq(featureIdsBadges.map((badge) => badge.text()))).toEqual(['Alerting', 'Cases']);
});
it('renders table with preconfigured connectors', async () => {
@ -272,10 +284,12 @@ describe('actions_connectors_list component empty with show only capability', ()
{
id: 'test',
name: 'Test',
supportedFeatureIds: ['alerting'],
},
{
id: 'test2',
name: 'Test2',
supportedFeatureIds: ['alerting'],
},
]);
const [
@ -335,10 +349,12 @@ describe('actions_connectors_list with show only capability', () => {
{
id: 'test',
name: 'Test',
supportedFeatureIds: ['alerting'],
},
{
id: 'test2',
name: 'Test2',
supportedFeatureIds: ['alerting'],
},
]);
const [
@ -406,6 +422,7 @@ describe('actions_connectors_list component with disabled items', () => {
enabled: false,
enabledInConfig: false,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
},
{
id: 'test2',
@ -413,6 +430,7 @@ describe('actions_connectors_list component with disabled items', () => {
enabled: false,
enabledInConfig: true,
enabledInLicense: false,
supportedFeatureIds: ['alerting'],
},
]);
@ -486,6 +504,7 @@ describe('actions_connectors_list component with deprecated connectors', () => {
enabled: false,
enabledInConfig: false,
enabledInLicense: true,
supportedFeatureIds: ['alerting'],
},
{
id: 'test2',
@ -493,6 +512,7 @@ describe('actions_connectors_list component with deprecated connectors', () => {
enabled: false,
enabledInConfig: true,
enabledInLicense: false,
supportedFeatureIds: ['alerting'],
},
]);

View file

@ -21,12 +21,13 @@ import {
EuiEmptyPrompt,
Criteria,
EuiButtonEmpty,
EuiBadge,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { omit } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import { withTheme, EuiTheme } from '@kbn/kibana-react-plugin/common';
import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants';
import { getConnectorFeatureName } from '@kbn/actions-plugin/common';
import { loadAllActions, loadActionTypes, deleteActions } from '../../../lib/action_connector_api';
import {
hasDeleteActionsCapability,
@ -130,23 +131,21 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
}, []);
const actionConnectorTableItems: ActionConnectorTableItem[] = actionTypesIndex
? actions
// TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502.
.filter((action) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(action.actionTypeId))
.map((action) => {
return {
...action,
actionType: actionTypesIndex[action.actionTypeId]
? actionTypesIndex[action.actionTypeId].name
: action.actionTypeId,
};
})
? actions.map((action) => {
return {
...action,
actionType: actionTypesIndex[action.actionTypeId]
? actionTypesIndex[action.actionTypeId].name
: action.actionTypeId,
featureIds: actionTypesIndex[action.actionTypeId]
? actionTypesIndex[action.actionTypeId].supportedFeatureIds
: [],
};
})
: [];
const actionTypesList: Array<{ value: string; name: string }> = actionTypesIndex
? Object.values(actionTypesIndex)
// TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502.
.filter((actionType) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(actionType.id))
.map((actionType) => ({
value: actionType.id,
name: `${actionType.name} (${getActionsCountByActionType(actions, actionType.id)})`,
@ -257,6 +256,30 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
sortable: false,
truncateText: true,
},
{
field: 'featureIds',
name: i18n.translate(
'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.featureIdsTitle',
{
defaultMessage: 'Availability',
}
),
sortable: false,
truncateText: true,
render: (availability: string[]) => {
return (
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
{(availability ?? []).map((featureId: string) => (
<EuiFlexItem grow={false} key={featureId}>
<EuiBadge data-test-subj="connectorsTableCell-featureIds" color="default">
{getConnectorFeatureName(featureId)}
</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
},
},
{
name: '',
render: (item: ActionConnectorTableItem) => {

View file

@ -207,6 +207,7 @@ describe('rule_details', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
];
@ -251,6 +252,7 @@ describe('rule_details', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: '.email',
@ -259,6 +261,7 @@ describe('rule_details', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
];
@ -337,6 +340,7 @@ describe('rule_details', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
];
ruleTypeRegistry.has.mockReturnValue(true);
@ -466,6 +470,7 @@ describe('rule_details', () => {
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
];
ruleTypeRegistry.has.mockReturnValue(true);

View file

@ -232,7 +232,7 @@ describe('rule_form', () => {
describe('rule_form create rule', () => {
let wrapper: ReactWrapper<any>;
async function setup(enforceMinimum = false, schedule = '1m') {
async function setup(enforceMinimum = false, schedule = '1m', featureId = 'alerting') {
const mocks = coreMock.createSetup();
const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types');
const ruleTypes: RuleType[] = [
@ -333,6 +333,7 @@ describe('rule_form', () => {
operation="create"
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
connectorFeatureId={featureId}
/>
);
@ -387,7 +388,15 @@ describe('rule_form', () => {
it('renders registered action types', async () => {
await setup();
const ruleTypeSelectOptions = wrapper.find(
'[data-test-subj=".server-log-ActionTypeSelectOption"]'
'[data-test-subj=".server-log-alerting-ActionTypeSelectOption"]'
);
expect(ruleTypeSelectOptions.exists()).toBeFalsy();
});
it('renders uses feature id to load action types', async () => {
await setup(false, '1m', 'anotherFeature');
const ruleTypeSelectOptions = wrapper.find(
'[data-test-subj=".server-log-anotherFeature-ActionTypeSelectOption"]'
);
expect(ruleTypeSelectOptions.exists()).toBeFalsy();
});

View file

@ -49,6 +49,7 @@ import {
RecoveredActionGroup,
isActionGroupDisabledForActionTypeId,
} from '@kbn/alerting-plugin/common';
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
import { RuleReducerAction, InitialRule } from './rule_reducer';
import {
RuleTypeModel,
@ -96,6 +97,7 @@ interface RuleFormProps<MetaData = Record<string, any>> {
setHasActionsWithBrokenConnector?: (value: boolean) => void;
metadata?: MetaData;
filteredRuleTypes?: string[];
connectorFeatureId?: string;
}
export const RuleForm = ({
@ -111,6 +113,7 @@ export const RuleForm = ({
actionTypeRegistry,
metadata,
filteredRuleTypes: ruleTypeToFilter,
connectorFeatureId = AlertingConnectorFeatureId,
}: RuleFormProps) => {
const {
notifications: { toasts },
@ -550,6 +553,7 @@ export const RuleForm = ({
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
messageVariables={selectedRuleType.actionVariables}
defaultActionGroupId={defaultActionGroupId}
featureId={connectorFeatureId}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) =>
isActionGroupDisabledForActionType(selectedRuleType, actionGroupId, actionTypeId)
}

View file

@ -38,6 +38,7 @@ import {
ALERTS_FEATURE_ID,
RuleExecutionStatusErrorReasons,
} from '@kbn/alerting-plugin/common';
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
import {
ActionType,
Rule,
@ -73,7 +74,6 @@ import { DeleteModalConfirmation } from '../../../components/delete_modal_confir
import { EmptyPrompt } from '../../../components/prompts/empty_prompt';
import { ALERT_STATUS_LICENSE_ERROR } from '../translations';
import { useKibana } from '../../../../common/lib/kibana';
import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants';
import './rules_list.scss';
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
import { ManageLicenseModal } from './manage_license_modal';
@ -325,13 +325,9 @@ export const RulesList = ({
useEffect(() => {
(async () => {
try {
const result = await loadActionTypes({ http });
const result = await loadActionTypes({ http, featureId: AlertingConnectorFeatureId });
const sortedResult = result
.filter(
// TODO: Remove "DEFAULT_HIDDEN_ACTION_TYPES" when cases connector is available across Kibana.
// Issue: https://github.com/elastic/kibana/issues/82502.
({ id }) => actionTypeRegistry.has(id) && !DEFAULT_HIDDEN_ACTION_TYPES.includes(id)
)
.filter(({ id }) => actionTypeRegistry.has(id))
.sort((a, b) => a.name.localeCompare(b.name));
setActionTypes(sortedResult);
} catch (e) {

View file

@ -10,7 +10,5 @@ export { AGGREGATION_TYPES, builtInAggregationTypes } from './aggregation_types'
export { builtInGroupByTypes } from './group_by_types';
export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions';
// TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502.
export const DEFAULT_HIDDEN_ACTION_TYPES = ['.case'];
export const PLUGIN_ID = 'triggersActions';

View file

@ -241,6 +241,7 @@ export type ActionConnectorWithoutId<
export type ActionConnectorTableItem = ActionConnector & {
actionType: ActionType['name'];
featureIds: ActionType['supportedFeatureIds'];
};
type AsActionVariables<Keys extends string> = {

View file

@ -92,6 +92,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
id: 'test.not-enabled',
name: 'Test: Not Enabled',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
async executor() {
return { status: 'ok', actionId: '' };
},

View file

@ -23,6 +23,7 @@ export function defineActionTypes(
id: 'test.noop',
name: 'Test: Noop',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
async executor() {
return { status: 'ok', actionId: '' };
},
@ -32,6 +33,7 @@ export function defineActionTypes(
id: 'test.throw',
name: 'Test: Throw',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
async executor() {
throw new Error('this action is intended to fail');
},
@ -41,6 +43,7 @@ export function defineActionTypes(
id: 'test.capped',
name: 'Test: Capped',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
async executor() {
return { status: 'ok', actionId: '' };
},
@ -82,6 +85,7 @@ function getIndexRecordActionType() {
id: 'test.index-record',
name: 'Test: Index Record',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
validate: {
params: paramsSchema,
config: configSchema,
@ -122,6 +126,7 @@ function getDelayedActionType() {
id: 'test.delayed',
name: 'Test: Delayed',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
validate: {
params: paramsSchema,
config: configSchema,
@ -149,6 +154,7 @@ function getFailingActionType() {
id: 'test.failing',
name: 'Test: Failing',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
validate: {
params: paramsSchema,
},
@ -181,6 +187,7 @@ function getRateLimitedActionType() {
id: 'test.rate-limit',
name: 'Test: Rate Limit',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
maxAttempts: 2,
validate: {
params: paramsSchema,
@ -217,6 +224,7 @@ function getNoAttemptsRateLimitedActionType() {
id: 'test.no-attempts-rate-limit',
name: 'Test: Rate Limit',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
maxAttempts: 0,
validate: {
params: paramsSchema,
@ -256,6 +264,7 @@ function getAuthorizationActionType(core: CoreSetup<FixtureStartDeps>) {
id: 'test.authorization',
name: 'Test: Authorization',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
validate: {
params: paramsSchema,
},
@ -335,6 +344,7 @@ function getExcludedActionType() {
id: 'test.excluded',
name: 'Test: Excluded',
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
async executor({ actionId }) {
return { status: 'ok', actionId };
},

View file

@ -83,6 +83,7 @@ export const getTestSubActionConnector = (
id: '.test-sub-action-connector',
name: 'Test: Sub action connector',
minimumLicenseRequired: 'platinum' as const,
supportedFeatureIds: ['alerting'],
schema: { config: TestConfigSchema, secrets: TestSecretsSchema },
Service: TestSubActionConnector,
};
@ -103,6 +104,7 @@ export const getTestSubActionConnectorWithoutSubActions = (
id: '.test-sub-action-connector-without-sub-actions',
name: 'Test: Sub action connector',
minimumLicenseRequired: 'platinum' as const,
supportedFeatureIds: ['alerting'],
schema: { config: TestConfigSchema, secrets: TestSecretsSchema },
Service: TestNoSubActions,
};

View file

@ -150,7 +150,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.setValue('ruleNameInput', alertName);
await testSubjects.click('thresholdPopover');
await testSubjects.setValue('alertThresholdInput', '3');
await testSubjects.click('.index-ActionTypeSelectOption');
await testSubjects.click('.index-alerting-ActionTypeSelectOption');
await monacoEditor.setCodeEditorValue(`{
"rule_id": "{{ruleId}}",

View file

@ -134,7 +134,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.click('onThrottleInterval');
await testSubjects.setValue('throttleInput', '10');
await testSubjects.click('.slack-ActionTypeSelectOption');
await testSubjects.click('.slack-alerting-ActionTypeSelectOption');
await testSubjects.click('addNewActionConnectorButton-.slack');
const slackConnectorName = generateUniqueKey();
await testSubjects.setValue('nameInput', slackConnectorName);
@ -188,7 +188,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await defineAlwaysFiringAlert(alertName);
// create Slack connector and attach an action using it
await testSubjects.click('.slack-ActionTypeSelectOption');
await testSubjects.click('.slack-alerting-ActionTypeSelectOption');
await testSubjects.click('addNewActionConnectorButton-.slack');
const slackConnectorName = generateUniqueKey();
await testSubjects.setValue('nameInput', slackConnectorName);
@ -204,7 +204,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
).type('some text ');
await testSubjects.click('addAlertActionButton');
await testSubjects.click('.slack-ActionTypeSelectOption');
await testSubjects.click('.slack-alerting-ActionTypeSelectOption');
await testSubjects.setValue('messageTextArea', 'test message ');
await (
await find.byCssSelector(