mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Extended existing alerting functionality to support preconfigured only action types (#64030)
* Extended existing alerting functionality to support preconfigured only action types * fixed functional test * Adding documentation * Fixed UI part due to comments * added missing tests * fixed action type execution * Fixed documentation * Fixed due to comments * fixed type checks * extended isActionExecutable to check exact action id if it is in the preconfigured list
This commit is contained in:
parent
74bf87721f
commit
6bf0e731b6
28 changed files with 676 additions and 178 deletions
|
@ -41,12 +41,14 @@ see https://www.elastic.co/subscriptions[the subscription page].
|
|||
|
||||
[float]
|
||||
[[create-connectors]]
|
||||
=== Connectors
|
||||
=== Preconfigured connectors and action types
|
||||
|
||||
You can create connectors for actions in <<managing-alerts-and-actions, Alerts and Actions>> or via the action API.
|
||||
For out-of-the-box and standardized connectors, you can <<pre-configured-connectors, preconfigure connectors>>
|
||||
before {kib} starts.
|
||||
|
||||
Action type with only preconfigured connectors could be specified as a <<pre-configured-action-types, preconfigured action type>>.
|
||||
|
||||
include::action-types/email.asciidoc[]
|
||||
include::action-types/index.asciidoc[]
|
||||
include::action-types/pagerduty.asciidoc[]
|
||||
|
@ -54,3 +56,4 @@ include::action-types/server-log.asciidoc[]
|
|||
include::action-types/slack.asciidoc[]
|
||||
include::action-types/webhook.asciidoc[]
|
||||
include::pre-configured-connectors.asciidoc[]
|
||||
include::pre-configured-action-types.asciidoc[]
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
After Width: | Height: | Size: 135 KiB |
Binary file not shown.
After Width: | Height: | Size: 268 KiB |
61
docs/user/alerting/pre-configured-action-types.asciidoc
Normal file
61
docs/user/alerting/pre-configured-action-types.asciidoc
Normal file
|
@ -0,0 +1,61 @@
|
|||
[role="xpack"]
|
||||
[[pre-configured-action-types]]
|
||||
|
||||
== Preconfigured action types
|
||||
|
||||
A preconfigure an action type has all the information it needs prior to startup.
|
||||
A preconfigured action type offers the following capabilities:
|
||||
|
||||
- Requires no setup. Configuration and credentials needed to execute an
|
||||
action are predefined.
|
||||
- Has only <<pre-configured-connectors, preconfigured connectors>>.
|
||||
- Connectors of the preconfigured action type cannot be edited or deleted.
|
||||
|
||||
[float]
|
||||
[[preconfigured-action-type-example]]
|
||||
=== Creating a preconfigured action
|
||||
|
||||
In the `kibana.yml` file:
|
||||
|
||||
. Exclude the action type from `xpack.actions.enabledActionTypes`.
|
||||
. Add all its connectors.
|
||||
|
||||
The following example shows a valid configuration of preconfigured action type with one out-of-the box connector.
|
||||
|
||||
```js
|
||||
xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1>
|
||||
xpack.actions.preconfigured: <2>
|
||||
- id: 'my-server-log'
|
||||
actionTypeId: .server-log
|
||||
name: 'Server log #xyz'
|
||||
```
|
||||
|
||||
<1> `enabledActionTypes` should exclude preconfigured action type to prevent creating and deleting connectors.
|
||||
<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type.
|
||||
|
||||
[float]
|
||||
[[pre-configured-action-type-alert-form]]
|
||||
=== Attaching a preconfigured action to an alert
|
||||
|
||||
To attach an action to an alert,
|
||||
select from a list of available action types, and
|
||||
then select the *Server log* type. This action type was configured previously.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/pre-configured-action-type-alert-form.png[Create alert with selected Server log action type]
|
||||
|
||||
[float]
|
||||
[[managing-pre-configured-action-types]]
|
||||
=== Managing preconfigured actions
|
||||
|
||||
Connectors with preconfigured actions appear in the connector list, regardless of which space the user is in.
|
||||
They are tagged as “preconfigured” and cannot be deleted.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured]
|
||||
|
||||
Clicking *Create connector* shows the list of available action types.
|
||||
Preconfigured action types are not included because you can't create a connector with a preconfigured action type.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu]
|
|
@ -14,6 +14,7 @@ const createActionTypeRegistryMock = () => {
|
|||
list: jest.fn(),
|
||||
ensureActionTypeEnabled: jest.fn(),
|
||||
isActionTypeEnabled: jest.fn(),
|
||||
isActionExecutable: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -28,6 +28,16 @@ beforeEach(() => {
|
|||
),
|
||||
actionsConfigUtils: mockedActionsConfig,
|
||||
licenseState: mockedLicenseState,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
actionTypeId: 'foo',
|
||||
config: {},
|
||||
id: 'my-slack1',
|
||||
name: 'Slack #xyz',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -194,6 +204,19 @@ describe('isActionTypeEnabled', () => {
|
|||
expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
|
||||
test('should call isActionExecutable of the actions config', async () => {
|
||||
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
|
||||
actionTypeRegistry.isActionExecutable('my-slack1', 'foo');
|
||||
expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
|
||||
test('should return true when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has preconfigured connectors', async () => {
|
||||
mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false);
|
||||
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
|
||||
|
||||
expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true);
|
||||
});
|
||||
|
||||
test('should call isLicenseValidForActionType of the license state', async () => {
|
||||
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
|
||||
actionTypeRegistry.isActionTypeEnabled('foo');
|
||||
|
|
|
@ -8,7 +8,7 @@ import Boom from 'boom';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { RunContext, TaskManagerSetupContract } from '../../task_manager/server';
|
||||
import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib';
|
||||
import { ActionType } from './types';
|
||||
import { ActionType, PreConfiguredAction } from './types';
|
||||
import { ActionType as CommonActionType } from '../common';
|
||||
import { ActionsConfigurationUtilities } from './actions_config';
|
||||
|
||||
|
@ -17,6 +17,7 @@ export interface ActionTypeRegistryOpts {
|
|||
taskRunnerFactory: TaskRunnerFactory;
|
||||
actionsConfigUtils: ActionsConfigurationUtilities;
|
||||
licenseState: ILicenseState;
|
||||
preconfiguredActions: PreConfiguredAction[];
|
||||
}
|
||||
|
||||
export class ActionTypeRegistry {
|
||||
|
@ -25,12 +26,14 @@ export class ActionTypeRegistry {
|
|||
private readonly taskRunnerFactory: TaskRunnerFactory;
|
||||
private readonly actionsConfigUtils: ActionsConfigurationUtilities;
|
||||
private readonly licenseState: ILicenseState;
|
||||
private readonly preconfiguredActions: PreConfiguredAction[];
|
||||
|
||||
constructor(constructorParams: ActionTypeRegistryOpts) {
|
||||
this.taskManager = constructorParams.taskManager;
|
||||
this.taskRunnerFactory = constructorParams.taskRunnerFactory;
|
||||
this.actionsConfigUtils = constructorParams.actionsConfigUtils;
|
||||
this.licenseState = constructorParams.licenseState;
|
||||
this.preconfiguredActions = constructorParams.preconfiguredActions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,6 +61,19 @@ export class ActionTypeRegistry {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if action type is enabled or it is a preconfigured action type.
|
||||
*/
|
||||
public isActionExecutable(actionId: string, actionTypeId: string) {
|
||||
return (
|
||||
this.isActionTypeEnabled(actionTypeId) ||
|
||||
(!this.isActionTypeEnabled(actionTypeId) &&
|
||||
this.preconfiguredActions.find(
|
||||
preconfiguredAction => preconfiguredAction.id === actionId
|
||||
) !== undefined)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an action type to the action type registry
|
||||
*/
|
||||
|
|
|
@ -44,6 +44,7 @@ beforeEach(() => {
|
|||
),
|
||||
actionsConfigUtils: actionsConfigMock.create(),
|
||||
licenseState: mockedLicenseState,
|
||||
preconfiguredActions: [],
|
||||
};
|
||||
actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
|
||||
actionsClient = new ActionsClient({
|
||||
|
@ -221,6 +222,7 @@ describe('create()', () => {
|
|||
),
|
||||
actionsConfigUtils: localConfigUtils,
|
||||
licenseState: licenseStateMock.create(),
|
||||
preconfiguredActions: [],
|
||||
};
|
||||
|
||||
actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams);
|
||||
|
|
|
@ -27,6 +27,7 @@ export function createActionTypeRegistry(): {
|
|||
),
|
||||
actionsConfigUtils: actionsConfigMock.create(),
|
||||
licenseState: licenseStateMock.create(),
|
||||
preconfiguredActions: [],
|
||||
});
|
||||
registerBuiltInActionTypes({
|
||||
logger,
|
||||
|
|
|
@ -282,4 +282,65 @@ describe('execute()', () => {
|
|||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`);
|
||||
});
|
||||
|
||||
test('should skip ensure action type if action type is preconfigured and license is valid', async () => {
|
||||
const mockedActionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient);
|
||||
const executeFn = createExecuteFunction({
|
||||
getBasePath,
|
||||
taskManager: mockTaskManager,
|
||||
getScopedSavedObjectsClient,
|
||||
isESOUsingEphemeralEncryptionKey: false,
|
||||
actionTypeRegistry: mockedActionTypeRegistry,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
actionTypeId: 'mock-action',
|
||||
config: {},
|
||||
id: 'my-slack1',
|
||||
name: 'Slack #xyz',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => {
|
||||
throw new Error('Fail');
|
||||
});
|
||||
mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'mock-action',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '234',
|
||||
type: 'action_task_params',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await executeFn({
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
apiKey: null,
|
||||
});
|
||||
expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {},
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -70,7 +70,9 @@ export function createExecuteFunction({
|
|||
const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest as KibanaRequest);
|
||||
const actionTypeId = await getActionTypeId(id);
|
||||
|
||||
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) {
|
||||
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
}
|
||||
|
||||
const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', {
|
||||
actionId: id,
|
||||
|
|
|
@ -224,6 +224,50 @@ test('throws an error if actionType is not enabled', async () => {
|
|||
expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
test('should not throws an error if actionType is preconfigured', async () => {
|
||||
const actionType: jest.Mocked<ActionType> = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
minimumLicenseRequired: 'basic',
|
||||
executor: jest.fn(),
|
||||
};
|
||||
const actionSavedObject = {
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'test',
|
||||
config: {
|
||||
bar: true,
|
||||
},
|
||||
secrets: {
|
||||
baz: true,
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
|
||||
throw new Error('not enabled for test');
|
||||
});
|
||||
actionTypeRegistry.isActionExecutable.mockImplementationOnce(() => true);
|
||||
await actionExecutor.execute(executeParams);
|
||||
|
||||
expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledTimes(0);
|
||||
expect(actionType.executor).toHaveBeenCalledWith({
|
||||
actionId: '1',
|
||||
services: expect.anything(),
|
||||
config: {
|
||||
bar: true,
|
||||
},
|
||||
secrets: {
|
||||
baz: true,
|
||||
},
|
||||
params: { foo: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => {
|
||||
const customActionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: true });
|
||||
customActionExecutor.initialize({
|
||||
|
|
|
@ -90,7 +90,9 @@ export class ActionExecutor {
|
|||
namespace.namespace
|
||||
);
|
||||
|
||||
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) {
|
||||
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
}
|
||||
const actionType = actionTypeRegistry.get(actionTypeId);
|
||||
|
||||
let validatedParams: Record<string, unknown>;
|
||||
|
|
|
@ -20,6 +20,7 @@ const createStartMock = () => {
|
|||
const mock: jest.Mocked<PluginStartContract> = {
|
||||
execute: jest.fn(),
|
||||
isActionTypeEnabled: jest.fn(),
|
||||
isActionExecutable: jest.fn(),
|
||||
getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()),
|
||||
preconfiguredActions: [],
|
||||
};
|
||||
|
|
|
@ -65,6 +65,7 @@ export interface PluginSetupContract {
|
|||
|
||||
export interface PluginStartContract {
|
||||
isActionTypeEnabled(id: string): boolean;
|
||||
isActionExecutable(actionId: string, actionTypeId: string): boolean;
|
||||
execute(options: ExecuteOptions): Promise<void>;
|
||||
getActionsClientWithRequest(request: KibanaRequest): Promise<PublicMethodsOf<ActionsClient>>;
|
||||
preconfiguredActions: PreConfiguredAction[];
|
||||
|
@ -170,6 +171,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
taskManager: plugins.taskManager,
|
||||
actionsConfigUtils,
|
||||
licenseState: this.licenseState,
|
||||
preconfiguredActions: this.preconfiguredActions,
|
||||
});
|
||||
this.taskRunnerFactory = taskRunnerFactory;
|
||||
this.actionTypeRegistry = actionTypeRegistry;
|
||||
|
@ -271,6 +273,9 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
isActionTypeEnabled: id => {
|
||||
return this.actionTypeRegistry!.isActionTypeEnabled(id);
|
||||
},
|
||||
isActionExecutable: (actionId: string, actionTypeId: string) => {
|
||||
return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId);
|
||||
},
|
||||
// Ability to get an actions client from legacy code
|
||||
async getActionsClientWithRequest(request: KibanaRequest) {
|
||||
if (isESOUsingEphemeralEncryptionKey === true) {
|
||||
|
|
|
@ -51,6 +51,7 @@ const createExecutionHandlerParams = {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
|
||||
createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
});
|
||||
|
||||
test('calls actionsPlugin.execute per selected action', async () => {
|
||||
|
@ -111,6 +112,7 @@ test('calls actionsPlugin.execute per selected action', async () => {
|
|||
|
||||
test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => {
|
||||
// Mock two calls, one for check against actions[0] and the second for actions[1]
|
||||
createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValueOnce(false);
|
||||
createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false);
|
||||
createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true);
|
||||
const executionHandler = createExecutionHandler({
|
||||
|
@ -148,6 +150,50 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () =>
|
|||
});
|
||||
});
|
||||
|
||||
test('trow error error message when action type is disabled', async () => {
|
||||
createExecutionHandlerParams.actionsPlugin.preconfiguredActions = [];
|
||||
createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(false);
|
||||
createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(false);
|
||||
const executionHandler = createExecutionHandler({
|
||||
...createExecutionHandlerParams,
|
||||
actions: [
|
||||
...createExecutionHandlerParams.actions,
|
||||
{
|
||||
id: '2',
|
||||
group: 'default',
|
||||
actionTypeId: '.slack',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My other {{context.value}} goes here',
|
||||
stateVal: 'My other {{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await executionHandler({
|
||||
actionGroup: 'default',
|
||||
state: {},
|
||||
context: {},
|
||||
alertInstanceId: '2',
|
||||
});
|
||||
|
||||
expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(0);
|
||||
|
||||
createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockImplementation(() => true);
|
||||
const executionHandlerForPreconfiguredAction = createExecutionHandler({
|
||||
...createExecutionHandlerParams,
|
||||
actions: [...createExecutionHandlerParams.actions],
|
||||
});
|
||||
await executionHandlerForPreconfiguredAction({
|
||||
actionGroup: 'default',
|
||||
state: {},
|
||||
context: {},
|
||||
alertInstanceId: '2',
|
||||
});
|
||||
expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('limits actionsPlugin.execute per action group', async () => {
|
||||
const executionHandler = createExecutionHandler(createExecutionHandlerParams);
|
||||
await executionHandler({
|
||||
|
|
|
@ -71,7 +71,7 @@ export function createExecutionHandler({
|
|||
const alertLabel = `${alertType.id}:${alertId}: '${alertName}'`;
|
||||
|
||||
for (const action of actions) {
|
||||
if (!actionsPlugin.isActionTypeEnabled(action.actionTypeId)) {
|
||||
if (!actionsPlugin.isActionExecutable(action.id, action.actionTypeId)) {
|
||||
logger.warn(
|
||||
`Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled`
|
||||
);
|
||||
|
|
|
@ -185,6 +185,7 @@ describe('Task Runner', () => {
|
|||
|
||||
test('actionsPlugin.execute is called per alert instance that is scheduled', async () => {
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
alertType.executor.mockImplementation(
|
||||
({ services: executorServices }: AlertExecutorOptions) => {
|
||||
executorServices.alertInstanceFactory('1').scheduleActions('default');
|
||||
|
|
|
@ -15943,7 +15943,6 @@
|
|||
"xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "アクションタイプを読み込み中...",
|
||||
"xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知間隔",
|
||||
"xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "アラートがアクティブな間にアクションを繰り返す頻度を定義します。",
|
||||
"xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}",
|
||||
"xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "アクション:アクションタイプを選択してください",
|
||||
"xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "トリガータイプを選択してください",
|
||||
"xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}",
|
||||
|
|
|
@ -15948,7 +15948,6 @@
|
|||
"xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "正在加载操作类型……",
|
||||
"xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知频率",
|
||||
"xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "定义告警处于活动状态时重复操作的频率。",
|
||||
"xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}",
|
||||
"xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "操作:选择操作类型",
|
||||
"xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "选择触发器类型",
|
||||
"xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}",
|
||||
|
|
|
@ -33,11 +33,20 @@ test('should sort enabled action types first', async () => {
|
|||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'x-fourth',
|
||||
enabled: true,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
},
|
||||
];
|
||||
const result = [...actionTypes].sort(actionTypeCompare);
|
||||
expect(result[0]).toEqual(actionTypes[0]);
|
||||
expect(result[1]).toEqual(actionTypes[2]);
|
||||
expect(result[2]).toEqual(actionTypes[1]);
|
||||
expect(result[2]).toEqual(actionTypes[3]);
|
||||
expect(result[3]).toEqual(actionTypes[1]);
|
||||
});
|
||||
|
||||
test('should sort by name when all enabled', async () => {
|
||||
|
@ -66,9 +75,18 @@ test('should sort by name when all enabled', async () => {
|
|||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'x-fourth',
|
||||
enabled: true,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
},
|
||||
];
|
||||
const result = [...actionTypes].sort(actionTypeCompare);
|
||||
expect(result[0]).toEqual(actionTypes[1]);
|
||||
expect(result[1]).toEqual(actionTypes[2]);
|
||||
expect(result[2]).toEqual(actionTypes[0]);
|
||||
expect(result[3]).toEqual(actionTypes[3]);
|
||||
});
|
||||
|
|
|
@ -4,14 +4,35 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionType } from '../../types';
|
||||
import { ActionType, ActionConnector } from '../../types';
|
||||
|
||||
export function actionTypeCompare(a: ActionType, b: ActionType) {
|
||||
if (a.enabled === true && b.enabled === false) {
|
||||
export function actionTypeCompare(
|
||||
a: ActionType,
|
||||
b: ActionType,
|
||||
preconfiguredConnectors?: ActionConnector[]
|
||||
) {
|
||||
const aEnabled = getIsEnabledValue(a, preconfiguredConnectors);
|
||||
const bEnabled = getIsEnabledValue(b, preconfiguredConnectors);
|
||||
|
||||
if (aEnabled === true && bEnabled === false) {
|
||||
return -1;
|
||||
}
|
||||
if (a.enabled === false && b.enabled === true) {
|
||||
if (aEnabled === false && bEnabled === true) {
|
||||
return 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
const getIsEnabledValue = (actionType: ActionType, preconfiguredConnectors?: ActionConnector[]) => {
|
||||
let isEnabled = actionType.enabled;
|
||||
if (
|
||||
!actionType.enabledInConfig &&
|
||||
preconfiguredConnectors &&
|
||||
preconfiguredConnectors.length > 0
|
||||
) {
|
||||
isEnabled =
|
||||
preconfiguredConnectors.find(connector => connector.actionTypeId === actionType.id) !==
|
||||
undefined;
|
||||
}
|
||||
return isEnabled;
|
||||
};
|
||||
|
|
|
@ -4,43 +4,47 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionType } from '../../types';
|
||||
import { checkActionTypeEnabled } from './check_action_type_enabled';
|
||||
import { ActionType, ActionConnector } from '../../types';
|
||||
import {
|
||||
checkActionTypeEnabled,
|
||||
checkActionFormActionTypeEnabled,
|
||||
} from './check_action_type_enabled';
|
||||
|
||||
test(`returns isEnabled:true when action type isn't provided`, async () => {
|
||||
expect(checkActionTypeEnabled()).toMatchInlineSnapshot(`
|
||||
describe('checkActionTypeEnabled', () => {
|
||||
test(`returns isEnabled:true when action type isn't provided`, async () => {
|
||||
expect(checkActionTypeEnabled()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"isEnabled": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns isEnabled:true when action type is enabled', async () => {
|
||||
const actionType: ActionType = {
|
||||
id: '1',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'my action',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
};
|
||||
expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
|
||||
test('returns isEnabled:true when action type is enabled', async () => {
|
||||
const actionType: ActionType = {
|
||||
id: '1',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'my action',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
};
|
||||
expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"isEnabled": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns isEnabled:false when action type is disabled by license', async () => {
|
||||
const actionType: ActionType = {
|
||||
id: '1',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'my action',
|
||||
enabled: false,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: false,
|
||||
};
|
||||
expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
|
||||
test('returns isEnabled:false when action type is disabled by license', async () => {
|
||||
const actionType: ActionType = {
|
||||
id: '1',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'my action',
|
||||
enabled: false,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: false,
|
||||
};
|
||||
expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"isEnabled": false,
|
||||
"message": "This connector requires a Basic license.",
|
||||
|
@ -63,18 +67,18 @@ test('returns isEnabled:false when action type is disabled by license', async ()
|
|||
</EuiCard>,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns isEnabled:false when action type is disabled by config', async () => {
|
||||
const actionType: ActionType = {
|
||||
id: '1',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'my action',
|
||||
enabled: false,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
};
|
||||
expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
|
||||
test('returns isEnabled:false when action type is disabled by config', async () => {
|
||||
const actionType: ActionType = {
|
||||
id: '1',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'my action',
|
||||
enabled: false,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
};
|
||||
expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"isEnabled": false,
|
||||
"message": "This connector is disabled by the Kibana configuration.",
|
||||
|
@ -85,4 +89,69 @@ test('returns isEnabled:false when action type is disabled by config', async ()
|
|||
/>,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkActionFormActionTypeEnabled', () => {
|
||||
const preconfiguredConnectors: ActionConnector[] = [
|
||||
{
|
||||
actionTypeId: '1',
|
||||
config: {},
|
||||
id: 'test1',
|
||||
isPreconfigured: true,
|
||||
name: 'test',
|
||||
secrets: {},
|
||||
referencedByCount: 0,
|
||||
},
|
||||
{
|
||||
actionTypeId: '2',
|
||||
config: {},
|
||||
id: 'test2',
|
||||
isPreconfigured: true,
|
||||
name: 'test',
|
||||
secrets: {},
|
||||
referencedByCount: 0,
|
||||
},
|
||||
];
|
||||
|
||||
test('returns isEnabled:true when action type is preconfigured', async () => {
|
||||
const actionType: ActionType = {
|
||||
id: '1',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'my action',
|
||||
enabled: true,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
};
|
||||
|
||||
expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"isEnabled": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns isEnabled:false when action type is disabled by config and not preconfigured', async () => {
|
||||
const actionType: ActionType = {
|
||||
id: 'disabled-by-config',
|
||||
minimumLicenseRequired: 'basic',
|
||||
name: 'my action',
|
||||
enabled: true,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
};
|
||||
expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"isEnabled": false,
|
||||
"message": "This connector is disabled by the Kibana configuration.",
|
||||
"messageCard": <EuiCard
|
||||
className="actCheckActionTypeEnabled__disabledActionWarningCard"
|
||||
description=""
|
||||
title="This feature is disabled by the Kibana configuration."
|
||||
/>,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import { capitalize } from 'lodash';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiCard, EuiLink } from '@elastic/eui';
|
||||
import { ActionType } from '../../types';
|
||||
import { ActionType, ActionConnector } from '../../types';
|
||||
import { VIEW_LICENSE_OPTIONS_LINK } from '../../common/constants';
|
||||
import './check_action_type_enabled.scss';
|
||||
|
||||
|
@ -22,71 +22,98 @@ export interface IsDisabledResult {
|
|||
messageCard: JSX.Element;
|
||||
}
|
||||
|
||||
const getLicenseCheckResult = (actionType: ActionType) => {
|
||||
return {
|
||||
isEnabled: false,
|
||||
message: i18n.translate(
|
||||
'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage',
|
||||
{
|
||||
defaultMessage: 'This connector requires a {minimumLicenseRequired} license.',
|
||||
values: {
|
||||
minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired),
|
||||
},
|
||||
}
|
||||
),
|
||||
messageCard: (
|
||||
<EuiCard
|
||||
titleSize="xs"
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageTitle',
|
||||
{
|
||||
defaultMessage: 'This feature requires a {minimumLicenseRequired} license.',
|
||||
values: {
|
||||
minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired),
|
||||
},
|
||||
}
|
||||
)}
|
||||
// The "re-enable" terminology is used here because this message is used when an alert
|
||||
// action was previously enabled and needs action to be re-enabled.
|
||||
description={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageDescription',
|
||||
{ defaultMessage: 'To re-enable this action, please upgrade your license.' }
|
||||
)}
|
||||
className="actCheckActionTypeEnabled__disabledActionWarningCard"
|
||||
children={
|
||||
<EuiLink href={VIEW_LICENSE_OPTIONS_LINK} target="_blank">
|
||||
<FormattedMessage
|
||||
defaultMessage="View license options"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseLinkTitle"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const configurationCheckResult = {
|
||||
isEnabled: false,
|
||||
message: i18n.translate(
|
||||
'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage',
|
||||
{ defaultMessage: 'This connector is disabled by the Kibana configuration.' }
|
||||
),
|
||||
messageCard: (
|
||||
<EuiCard
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByConfigMessageTitle',
|
||||
{ defaultMessage: 'This feature is disabled by the Kibana configuration.' }
|
||||
)}
|
||||
description=""
|
||||
className="actCheckActionTypeEnabled__disabledActionWarningCard"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export function checkActionTypeEnabled(
|
||||
actionType?: ActionType
|
||||
): IsEnabledResult | IsDisabledResult {
|
||||
if (actionType?.enabledInLicense === false) {
|
||||
return {
|
||||
isEnabled: false,
|
||||
message: i18n.translate(
|
||||
'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage',
|
||||
{
|
||||
defaultMessage: 'This connector requires a {minimumLicenseRequired} license.',
|
||||
values: {
|
||||
minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired),
|
||||
},
|
||||
}
|
||||
),
|
||||
messageCard: (
|
||||
<EuiCard
|
||||
titleSize="xs"
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageTitle',
|
||||
{
|
||||
defaultMessage: 'This feature requires a {minimumLicenseRequired} license.',
|
||||
values: {
|
||||
minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired),
|
||||
},
|
||||
}
|
||||
)}
|
||||
// The "re-enable" terminology is used here because this message is used when an alert
|
||||
// action was previously enabled and needs action to be re-enabled.
|
||||
description={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageDescription',
|
||||
{ defaultMessage: 'To re-enable this action, please upgrade your license.' }
|
||||
)}
|
||||
className="actCheckActionTypeEnabled__disabledActionWarningCard"
|
||||
children={
|
||||
<EuiLink href={VIEW_LICENSE_OPTIONS_LINK} target="_blank">
|
||||
<FormattedMessage
|
||||
defaultMessage="View license options"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseLinkTitle"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
return getLicenseCheckResult(actionType);
|
||||
}
|
||||
|
||||
if (actionType?.enabledInConfig === false) {
|
||||
return {
|
||||
isEnabled: false,
|
||||
message: i18n.translate(
|
||||
'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage',
|
||||
{ defaultMessage: 'This connector is disabled by the Kibana configuration.' }
|
||||
),
|
||||
messageCard: (
|
||||
<EuiCard
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByConfigMessageTitle',
|
||||
{ defaultMessage: 'This feature is disabled by the Kibana configuration.' }
|
||||
)}
|
||||
description=""
|
||||
className="actCheckActionTypeEnabled__disabledActionWarningCard"
|
||||
/>
|
||||
),
|
||||
};
|
||||
return configurationCheckResult;
|
||||
}
|
||||
|
||||
return { isEnabled: true };
|
||||
}
|
||||
|
||||
export function checkActionFormActionTypeEnabled(
|
||||
actionType: ActionType,
|
||||
preconfiguredConnectors: ActionConnector[]
|
||||
): IsEnabledResult | IsDisabledResult {
|
||||
if (actionType?.enabledInLicense === false) {
|
||||
return getLicenseCheckResult(actionType);
|
||||
}
|
||||
|
||||
if (
|
||||
actionType?.enabledInConfig === false &&
|
||||
// do not disable action type if it contains preconfigured connectors (is preconfigured)
|
||||
!preconfiguredConnectors.find(
|
||||
preconfiguredConnector => preconfiguredConnector.actionTypeId === actionType.id
|
||||
)
|
||||
) {
|
||||
return configurationCheckResult;
|
||||
}
|
||||
|
||||
return { isEnabled: true };
|
||||
|
|
|
@ -73,6 +73,21 @@ describe('action_form', () => {
|
|||
actionParamsFields: null,
|
||||
};
|
||||
|
||||
const preconfiguredOnly = {
|
||||
id: 'preconfigured',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ValidationResult => {
|
||||
return { errors: {} };
|
||||
},
|
||||
validateParams: (): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: null,
|
||||
};
|
||||
|
||||
describe('action_form in alert', () => {
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
||||
|
@ -95,6 +110,22 @@ describe('action_form', () => {
|
|||
config: {},
|
||||
isPreconfigured: true,
|
||||
},
|
||||
{
|
||||
secrets: {},
|
||||
id: 'test3',
|
||||
actionTypeId: preconfiguredOnly.id,
|
||||
name: 'Preconfigured Only',
|
||||
config: {},
|
||||
isPreconfigured: true,
|
||||
},
|
||||
{
|
||||
secrets: {},
|
||||
id: 'test4',
|
||||
actionTypeId: preconfiguredOnly.id,
|
||||
name: 'Regular connector',
|
||||
config: {},
|
||||
isPreconfigured: false,
|
||||
},
|
||||
]);
|
||||
const mockes = coreMock.createSetup();
|
||||
deps = {
|
||||
|
@ -106,6 +137,7 @@ describe('action_form', () => {
|
|||
actionType,
|
||||
disabledByConfigActionType,
|
||||
disabledByLicenseActionType,
|
||||
preconfiguredOnly,
|
||||
]);
|
||||
actionTypeRegistry.has.mockReturnValue(true);
|
||||
actionTypeRegistry.get.mockReturnValue(actionType);
|
||||
|
@ -166,6 +198,14 @@ describe('action_form', () => {
|
|||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'preconfigured',
|
||||
name: 'Preconfigured only',
|
||||
enabled: true,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'disabled-by-config',
|
||||
name: 'Disabled by config',
|
||||
|
@ -207,21 +247,27 @@ describe('action_form', () => {
|
|||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`doesn't render action types disabled by config`, async () => {
|
||||
it('does not render action types disabled by config', async () => {
|
||||
await setup();
|
||||
const actionOption = wrapper.find(
|
||||
`[data-test-subj="disabled-by-config-ActionTypeSelectOption"]`
|
||||
'[data-test-subj="disabled-by-config-ActionTypeSelectOption"]'
|
||||
);
|
||||
expect(actionOption.exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`renders available connectors for the selected action type`, async () => {
|
||||
it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => {
|
||||
await setup();
|
||||
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
|
||||
expect(actionOption.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders available connectors for the selected action type', async () => {
|
||||
await setup();
|
||||
const actionOption = wrapper.find(
|
||||
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
|
||||
);
|
||||
actionOption.first().simulate('click');
|
||||
const combobox = wrapper.find(`[data-test-subj="selectActionConnector"]`);
|
||||
const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`);
|
||||
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
|
@ -238,10 +284,37 @@ describe('action_form', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('renders only preconfigured connectors for the selected preconfigured action type', async () => {
|
||||
await setup();
|
||||
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
|
||||
actionOption.first().simulate('click');
|
||||
const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]');
|
||||
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "test3",
|
||||
"key": "test3",
|
||||
"label": "Preconfigured Only (preconfigured)",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not render "Add new" button for preconfigured only action type', async () => {
|
||||
await setup();
|
||||
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
|
||||
actionOption.first().simulate('click');
|
||||
const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]');
|
||||
const addNewConnectorButton = preconfigPannel.find(
|
||||
'[data-test-subj="addNewActionConnectorButton-preconfigured"]'
|
||||
);
|
||||
expect(addNewConnectorButton.exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders action types disabled by license', async () => {
|
||||
await setup();
|
||||
const actionOption = wrapper.find(
|
||||
`[data-test-subj="disabled-by-license-ActionTypeSelectOption"]`
|
||||
'[data-test-subj="disabled-by-license-ActionTypeSelectOption"]'
|
||||
);
|
||||
expect(actionOption.exists()).toBeTruthy();
|
||||
expect(
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api';
|
||||
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
|
||||
import {
|
||||
IErrorObject,
|
||||
ActionTypeModel,
|
||||
|
@ -42,7 +42,7 @@ import { SectionLoading } from '../../components/section_loading';
|
|||
import { ConnectorAddModal } from './connector_add_modal';
|
||||
import { TypeRegistry } from '../../type_registry';
|
||||
import { actionTypeCompare } from '../../lib/action_type_compare';
|
||||
import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled';
|
||||
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
|
||||
import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
|
||||
|
||||
interface ActionAccordionFormProps {
|
||||
|
@ -111,14 +111,12 @@ export const ActionForm = ({
|
|||
setHasActionsDisabled(hasActionsDisabled);
|
||||
}
|
||||
} catch (e) {
|
||||
if (toastNotifications) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage',
|
||||
{ defaultMessage: 'Unable to load action types' }
|
||||
),
|
||||
});
|
||||
}
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage',
|
||||
{ defaultMessage: 'Unable to load action types' }
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
setIsLoadingActionTypes(false);
|
||||
}
|
||||
|
@ -126,41 +124,50 @@ export const ActionForm = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// load connectors
|
||||
useEffect(() => {
|
||||
loadConnectors();
|
||||
(async () => {
|
||||
try {
|
||||
setIsLoadingConnectors(true);
|
||||
setConnectors(await loadConnectors({ http }));
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to load connectors',
|
||||
}
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
setIsLoadingConnectors(false);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
async function loadConnectors() {
|
||||
try {
|
||||
setIsLoadingConnectors(true);
|
||||
const actionsResponse = await loadAllActions({ http });
|
||||
setConnectors(actionsResponse);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to load connectors',
|
||||
}
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
setIsLoadingConnectors(false);
|
||||
}
|
||||
}
|
||||
const preconfiguredMessage = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
|
||||
{
|
||||
defaultMessage: '(preconfigured)',
|
||||
}
|
||||
);
|
||||
|
||||
const getSelectedOptions = (actionItemId: string) => {
|
||||
const val = connectors.find(connector => connector.id === actionItemId);
|
||||
if (!val) {
|
||||
const selectedConnector = connectors.find(connector => connector.id === actionItemId);
|
||||
if (
|
||||
!selectedConnector ||
|
||||
// if selected connector is not preconfigured and action type is for preconfiguration only,
|
||||
// do not show regular connectors of this type
|
||||
(actionTypesIndex &&
|
||||
!actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
|
||||
!selectedConnector.isPreconfigured)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const optionTitle = `${val.name} ${val.isPreconfigured ? preconfiguredMessage : ''}`;
|
||||
const optionTitle = `${selectedConnector.name} ${
|
||||
selectedConnector.isPreconfigured ? preconfiguredMessage : ''
|
||||
}`;
|
||||
return [
|
||||
{
|
||||
label: optionTitle,
|
||||
|
@ -179,8 +186,15 @@ export const ActionForm = ({
|
|||
},
|
||||
index: number
|
||||
) => {
|
||||
const actionType = actionTypesIndex![actionItem.actionTypeId];
|
||||
|
||||
const optionsList = connectors
|
||||
.filter(connectorItem => connectorItem.actionTypeId === actionItem.actionTypeId)
|
||||
.filter(
|
||||
connectorItem =>
|
||||
connectorItem.actionTypeId === actionItem.actionTypeId &&
|
||||
// include only enabled by config connectors or preconfigured
|
||||
(actionType.enabledInConfig || connectorItem.isPreconfigured)
|
||||
)
|
||||
.map(({ name, id, isPreconfigured }) => ({
|
||||
label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
|
||||
key: id,
|
||||
|
@ -189,8 +203,9 @@ export const ActionForm = ({
|
|||
const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
|
||||
if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
|
||||
const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
|
||||
const checkEnabledResult = checkActionTypeEnabled(
|
||||
actionTypesIndex && actionTypesIndex[actionConnector.actionTypeId]
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionTypesIndex![actionConnector.actionTypeId],
|
||||
connectors.filter(connector => connector.isPreconfigured)
|
||||
);
|
||||
|
||||
const accordionContent = checkEnabledResult.isEnabled ? (
|
||||
|
@ -211,19 +226,21 @@ export const ActionForm = ({
|
|||
/>
|
||||
}
|
||||
labelAppend={
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
data-test-subj="createActionConnectorButton"
|
||||
onClick={() => {
|
||||
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
|
||||
setAddModalVisibility(true);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add new"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
actionTypesIndex![actionConnector.actionTypeId].enabledInConfig ? (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
data-test-subj={`addNewActionConnectorButton-${actionItem.actionTypeId}`}
|
||||
onClick={() => {
|
||||
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
|
||||
setAddModalVisibility(true);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add new"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
|
@ -231,7 +248,7 @@ export const ActionForm = ({
|
|||
singleSelection={{ asPlainText: true }}
|
||||
options={optionsList}
|
||||
id={`selectActionConnector-${actionItem.id}`}
|
||||
data-test-subj="selectActionConnector"
|
||||
data-test-subj={`selectActionConnector-${actionItem.actionTypeId}`}
|
||||
selectedOptions={getSelectedOptions(actionItem.id)}
|
||||
onChange={selectedOptions => {
|
||||
setActionIdByIndex(selectedOptions[0].id ?? '', index);
|
||||
|
@ -258,10 +275,9 @@ export const ActionForm = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment key={index}>
|
||||
<EuiAccordion
|
||||
initialIsOpen={true}
|
||||
key={index}
|
||||
id={index.toString()}
|
||||
className="actAccordionActionForm"
|
||||
buttonContentClassName="actAccordionActionForm__button"
|
||||
|
@ -273,12 +289,12 @@ export const ActionForm = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<p>
|
||||
<div>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
defaultMessage="{actionConnectorName}"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.existingAlertActionTypeEditTitle"
|
||||
values={{
|
||||
actionConnectorName: `${actionConnector.name} ${
|
||||
actionConnector.isPreconfigured ? preconfiguredMessage : ''
|
||||
|
@ -304,7 +320,7 @@ export const ActionForm = ({
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</p>
|
||||
</div>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -349,10 +365,9 @@ export const ActionForm = ({
|
|||
const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
|
||||
if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment key={index}>
|
||||
<EuiAccordion
|
||||
initialIsOpen={true}
|
||||
key={index}
|
||||
id={index.toString()}
|
||||
className="actAccordionActionForm"
|
||||
buttonContentClassName="actAccordionActionForm__button"
|
||||
|
@ -364,15 +379,15 @@ export const ActionForm = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<p>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="{actionConnectorName}"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.newAlertActionTypeEditTitle"
|
||||
values={{
|
||||
actionConnectorName: actionTypeRegistered.actionTypeTitle,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -486,18 +501,26 @@ export const ActionForm = ({
|
|||
}
|
||||
}
|
||||
|
||||
let actionTypeNodes: JSX.Element[] | null = null;
|
||||
let actionTypeNodes: Array<JSX.Element | null> | null = null;
|
||||
let hasDisabledByLicenseActionTypes = false;
|
||||
if (actionTypesIndex) {
|
||||
const preconfiguredConnectors = connectors.filter(connector => connector.isPreconfigured);
|
||||
actionTypeNodes = actionTypeRegistry
|
||||
.list()
|
||||
.filter(
|
||||
item => actionTypesIndex[item.id] && actionTypesIndex[item.id].enabledInConfig === true
|
||||
.filter(item => actionTypesIndex[item.id])
|
||||
.sort((a, b) =>
|
||||
actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id], preconfiguredConnectors)
|
||||
)
|
||||
.sort((a, b) => actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id]))
|
||||
.map(function(item, index) {
|
||||
const actionType = actionTypesIndex[item.id];
|
||||
const checkEnabledResult = checkActionTypeEnabled(actionTypesIndex[item.id]);
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionTypesIndex[item.id],
|
||||
preconfiguredConnectors
|
||||
);
|
||||
// if action type is not enabled in config and not preconfigured, it shouldn't be displayed
|
||||
if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) {
|
||||
return null;
|
||||
}
|
||||
if (!actionType.enabledInLicense) {
|
||||
hasDisabledByLicenseActionTypes = true;
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await nameInput.click();
|
||||
|
||||
await testSubjects.click('.slack-ActionTypeSelectOption');
|
||||
await testSubjects.click('createActionConnectorButton');
|
||||
await testSubjects.click('addNewActionConnectorButton-.slack');
|
||||
const slackConnectorName = generateUniqueKey();
|
||||
await testSubjects.setValue('nameInput', slackConnectorName);
|
||||
await testSubjects.setValue('slackWebhookUrlInput', 'https://test');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue