mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Response Ops][Rule Form V2] Rule form v2: Actions Modal and Actions Form (#187434)](https://github.com/elastic/kibana/pull/187434) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Jiawei Wu","email":"74562234+JiaweiWu@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-09-27T06:34:27Z","message":"[Response Ops][Rule Form V2] Rule form v2: Actions Modal and Actions Form (#187434)\n\n## Summary\r\nIssue: https://github.com/elastic/kibana/issues/179105\r\nRelated PR: https://github.com/elastic/kibana/pull/180539\r\n\r\nFinal PR of the rule actions V2 PR (2/2 of the actions PRs). This PR\r\ncontains the actions modal and actions form. This PR depends on\r\nhttps://github.com/elastic/kibana/pull/186490.\r\n\r\nI have also created a example plugin to demonstrate this PR. To access:\r\n\r\n1. Run the branch with yarn start --run-examples\r\n2. Navigate to\r\nhttp://localhost:5601/app/triggersActionsUiExample/rule/create/<ruleTypeId>\r\n(I use .es-query)\r\n3. Create a rule\r\n4. Navigate to\r\nhttp://localhost:5601/app/triggersActionsUiExample/rule/edit/<ruleId>\r\nwith the rule you just created to edit the rule\r\n\r\n<img width=\"1236\" alt=\"Screenshot 2024-07-02 at 5 15 51 PM\"\r\nsrc=\"1dc5d2a9
-804a-4861-94ba-814de73dc3ab\">\r\n\r\n\r\n\r\n<img width=\"1087\" alt=\"Screenshot 2024-07-02 at 5 15 58 PM\"\r\nsrc=\"903e66b5
-f9a1-4d09-b121-b1dcecdff72c\">\r\n\r\n\r\n### Checklist\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"54659e8ae002aa68be3ee472ef12b3d3f546a1ea","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor","v8.16.0"],"title":"[Response Ops][Rule Form V2] Rule form v2: Actions Modal and Actions Form","number":187434,"url":"https://github.com/elastic/kibana/pull/187434","mergeCommit":{"message":"[Response Ops][Rule Form V2] Rule form v2: Actions Modal and Actions Form (#187434)\n\n## Summary\r\nIssue: https://github.com/elastic/kibana/issues/179105\r\nRelated PR: https://github.com/elastic/kibana/pull/180539\r\n\r\nFinal PR of the rule actions V2 PR (2/2 of the actions PRs). This PR\r\ncontains the actions modal and actions form. This PR depends on\r\nhttps://github.com/elastic/kibana/pull/186490.\r\n\r\nI have also created a example plugin to demonstrate this PR. To access:\r\n\r\n1. Run the branch with yarn start --run-examples\r\n2. Navigate to\r\nhttp://localhost:5601/app/triggersActionsUiExample/rule/create/<ruleTypeId>\r\n(I use .es-query)\r\n3. Create a rule\r\n4. Navigate to\r\nhttp://localhost:5601/app/triggersActionsUiExample/rule/edit/<ruleId>\r\nwith the rule you just created to edit the rule\r\n\r\n<img width=\"1236\" alt=\"Screenshot 2024-07-02 at 5 15 51 PM\"\r\nsrc=\"1dc5d2a9
-804a-4861-94ba-814de73dc3ab\">\r\n\r\n\r\n\r\n<img width=\"1087\" alt=\"Screenshot 2024-07-02 at 5 15 58 PM\"\r\nsrc=\"903e66b5
-f9a1-4d09-b121-b1dcecdff72c\">\r\n\r\n\r\n### Checklist\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"54659e8ae002aa68be3ee472ef12b3d3f546a1ea"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/187434","number":187434,"mergeCommit":{"message":"[Response Ops][Rule Form V2] Rule form v2: Actions Modal and Actions Form (#187434)\n\n## Summary\r\nIssue: https://github.com/elastic/kibana/issues/179105\r\nRelated PR: https://github.com/elastic/kibana/pull/180539\r\n\r\nFinal PR of the rule actions V2 PR (2/2 of the actions PRs). This PR\r\ncontains the actions modal and actions form. This PR depends on\r\nhttps://github.com/elastic/kibana/pull/186490.\r\n\r\nI have also created a example plugin to demonstrate this PR. To access:\r\n\r\n1. Run the branch with yarn start --run-examples\r\n2. Navigate to\r\nhttp://localhost:5601/app/triggersActionsUiExample/rule/create/<ruleTypeId>\r\n(I use .es-query)\r\n3. Create a rule\r\n4. Navigate to\r\nhttp://localhost:5601/app/triggersActionsUiExample/rule/edit/<ruleId>\r\nwith the rule you just created to edit the rule\r\n\r\n<img width=\"1236\" alt=\"Screenshot 2024-07-02 at 5 15 51 PM\"\r\nsrc=\"1dc5d2a9
-804a-4861-94ba-814de73dc3ab\">\r\n\r\n\r\n\r\n<img width=\"1087\" alt=\"Screenshot 2024-07-02 at 5 15 58 PM\"\r\nsrc=\"903e66b5
-f9a1-4d09-b121-b1dcecdff72c\">\r\n\r\n\r\n### Checklist\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"54659e8ae002aa68be3ee472ef12b3d3f546a1ea"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com>
This commit is contained in:
parent
c7b29d18dd
commit
d52ad0a173
79 changed files with 5944 additions and 500 deletions
|
@ -16,8 +16,9 @@ import { SEARCH_BAR_PLACEHOLDER } from './translations';
|
|||
import type { AlertsSearchBarProps, QueryLanguageType } from './types';
|
||||
import { useLoadRuleTypesQuery, useAlertsDataView, useRuleAADFields } from '../common/hooks';
|
||||
|
||||
const SA_ALERTS = { type: 'alerts', fields: {} } as SuggestionsAbstraction;
|
||||
export type { AlertsSearchBarProps } from './types';
|
||||
|
||||
const SA_ALERTS = { type: 'alerts', fields: {} } as SuggestionsAbstraction;
|
||||
const EMPTY_FEATURE_IDS: ValidFeatureId[] = [];
|
||||
|
||||
export const AlertsSearchBar = ({
|
||||
|
|
|
@ -10,3 +10,5 @@
|
|||
export * from './alerts';
|
||||
export * from './i18n_weekdays';
|
||||
export * from './routes';
|
||||
|
||||
export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions';
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
|
||||
export * from './use_alerts_data_view';
|
||||
export * from './use_create_rule';
|
||||
export * from './use_update_rule';
|
||||
export * from './use_resolve_rule';
|
||||
export * from './use_load_connectors';
|
||||
export * from './use_load_connector_types';
|
||||
export * from './use_get_alerts_group_aggregations_query';
|
||||
export * from './use_health_check';
|
||||
export * from './use_load_alerting_framework_health';
|
||||
|
|
|
@ -13,7 +13,7 @@ import { renderHook } from '@testing-library/react-hooks/dom';
|
|||
import { waitFor } from '@testing-library/react';
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
|
||||
import { useLoadActionTypes } from './use_load_connector_types';
|
||||
import { useLoadConnectorTypes } from './use_load_connector_types';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
@ -46,7 +46,7 @@ describe('useLoadConnectorTypes', () => {
|
|||
test('should call API endpoint with the correct parameters', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useLoadActionTypes({
|
||||
useLoadConnectorTypes({
|
||||
http,
|
||||
includeSystemActions: true,
|
||||
}),
|
||||
|
@ -74,7 +74,7 @@ describe('useLoadConnectorTypes', () => {
|
|||
test('should call the correct endpoint if system actions is true', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useLoadActionTypes({
|
||||
useLoadConnectorTypes({
|
||||
http,
|
||||
includeSystemActions: true,
|
||||
}),
|
||||
|
@ -91,7 +91,7 @@ describe('useLoadConnectorTypes', () => {
|
|||
test('should call the correct endpoint if system actions is false', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useLoadActionTypes({
|
||||
useLoadConnectorTypes({
|
||||
http,
|
||||
includeSystemActions: false,
|
||||
}),
|
||||
|
|
|
@ -11,13 +11,14 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import { fetchConnectorTypes } from '../apis';
|
||||
|
||||
export interface UseLoadActionTypesProps {
|
||||
export interface UseLoadConnectorTypesProps {
|
||||
http: HttpStart;
|
||||
includeSystemActions?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export const useLoadActionTypes = (props: UseLoadActionTypesProps) => {
|
||||
const { http, includeSystemActions } = props;
|
||||
export const useLoadConnectorTypes = (props: UseLoadConnectorTypesProps) => {
|
||||
const { http, includeSystemActions, enabled = true } = props;
|
||||
|
||||
const queryFn = () => {
|
||||
return fetchConnectorTypes({ http, includeSystemActions });
|
||||
|
@ -27,6 +28,7 @@ export const useLoadActionTypes = (props: UseLoadActionTypesProps) => {
|
|||
queryKey: ['useLoadConnectorTypes', includeSystemActions],
|
||||
queryFn,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -14,10 +14,11 @@ import { fetchConnectors } from '../apis';
|
|||
export interface UseLoadConnectorsProps {
|
||||
http: HttpStart;
|
||||
includeSystemActions?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export const useLoadConnectors = (props: UseLoadConnectorsProps) => {
|
||||
const { http, includeSystemActions = false } = props;
|
||||
const { http, includeSystemActions = false, enabled = true } = props;
|
||||
|
||||
const queryFn = () => {
|
||||
return fetchConnectors({ http, includeSystemActions });
|
||||
|
@ -27,6 +28,7 @@ export const useLoadConnectors = (props: UseLoadConnectorsProps) => {
|
|||
queryKey: ['useLoadConnectors', includeSystemActions],
|
||||
queryFn,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { fetchRuleTypeAadTemplateFields, getDescription } from '../apis';
|
|||
|
||||
export interface UseLoadRuleTypeAadTemplateFieldProps {
|
||||
http: HttpStart;
|
||||
ruleTypeId: string;
|
||||
ruleTypeId?: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,9 @@ export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplat
|
|||
const { http, ruleTypeId, enabled } = props;
|
||||
|
||||
const queryFn = () => {
|
||||
if (!ruleTypeId) {
|
||||
return;
|
||||
}
|
||||
return fetchRuleTypeAadTemplateFields({ http, ruleTypeId });
|
||||
};
|
||||
|
||||
|
@ -35,7 +38,7 @@ export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplat
|
|||
queryKey: ['useLoadRuleTypeAadTemplateField', ruleTypeId],
|
||||
queryFn,
|
||||
select: (dataViewFields) => {
|
||||
return dataViewFields.map<ActionVariable>((d) => ({
|
||||
return dataViewFields?.map<ActionVariable>((d) => ({
|
||||
name: d.name,
|
||||
description: getDescription(d.name, EcsFlat),
|
||||
}));
|
||||
|
|
|
@ -41,6 +41,7 @@ export const useResolveRule = (props: UseResolveProps) => {
|
|||
};
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ActionType } from '@kbn/actions-types';
|
||||
import { RuleSystemAction } from '@kbn/alerting-types';
|
||||
import { ActionConnector, ActionTypeModel, GenericValidationResult, RuleAction } from '../types';
|
||||
import { actionTypeRegistryMock } from './action_type_registry.mock';
|
||||
|
||||
export const getConnector = (
|
||||
id: string,
|
||||
overwrites?: Partial<ActionConnector>
|
||||
): ActionConnector => {
|
||||
return {
|
||||
id: `connector-${id}`,
|
||||
secrets: { secret: 'secret' },
|
||||
actionTypeId: `actionType-${id}`,
|
||||
name: `connector-${id}`,
|
||||
config: { config: `config-${id}` },
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
isDeprecated: false,
|
||||
...overwrites,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAction = (id: string, overwrites?: Partial<RuleAction>): RuleAction => {
|
||||
return {
|
||||
id: `action-${id}`,
|
||||
uuid: `uuid-action-${id}`,
|
||||
group: `group-${id}`,
|
||||
actionTypeId: `actionType-${id}`,
|
||||
params: {},
|
||||
...overwrites,
|
||||
};
|
||||
};
|
||||
|
||||
export const getSystemAction = (
|
||||
id: string,
|
||||
overwrites?: Partial<RuleSystemAction>
|
||||
): RuleSystemAction => {
|
||||
return {
|
||||
uuid: `uuid-system-action-${id}`,
|
||||
id: `system-action-${id}`,
|
||||
actionTypeId: `actionType-${id}`,
|
||||
params: {},
|
||||
...overwrites,
|
||||
};
|
||||
};
|
||||
|
||||
export const getActionType = (id: string, overwrites?: Partial<ActionType>): ActionType => {
|
||||
return {
|
||||
id: `actionType-${id}`,
|
||||
name: `actionType: ${id}`,
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
supportedFeatureIds: ['stackAlerts'],
|
||||
isSystemActionType: false,
|
||||
...overwrites,
|
||||
};
|
||||
};
|
||||
|
||||
export const getActionTypeModel = (
|
||||
id: string,
|
||||
overwrites?: Partial<ActionTypeModel>
|
||||
): ActionTypeModel => {
|
||||
return actionTypeRegistryMock.createMockActionTypeModel({
|
||||
id: `actionTypeModel-${id}`,
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
...overwrites,
|
||||
});
|
||||
};
|
|
@ -43,6 +43,10 @@ export interface RuleFormBaseErrors {
|
|||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface RuleFormActionsErrors {
|
||||
filterQuery?: string[];
|
||||
}
|
||||
|
||||
export interface RuleFormParamsErrors {
|
||||
[key: string]: string | string[] | RuleFormParamsErrors;
|
||||
}
|
||||
|
|
|
@ -32,10 +32,12 @@ export const GET_DEFAULT_FORM_DATA = ({
|
|||
name,
|
||||
consumer,
|
||||
schedule,
|
||||
actions,
|
||||
}: {
|
||||
ruleTypeId: RuleFormData['ruleTypeId'];
|
||||
name: RuleFormData['name'];
|
||||
consumer: RuleFormData['consumer'];
|
||||
actions: RuleFormData['actions'];
|
||||
schedule?: RuleFormData['schedule'];
|
||||
}) => {
|
||||
return {
|
||||
|
@ -47,6 +49,7 @@ export const GET_DEFAULT_FORM_DATA = ({
|
|||
consumer,
|
||||
ruleTypeId,
|
||||
name,
|
||||
actions,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { EuiLoadingElastic } from '@elastic/eui';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import type { RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
import { type RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
import type { RuleFormData, RuleFormPlugins } from './types';
|
||||
import { ALERTING_FEATURE_ID, DEFAULT_VALID_CONSUMERS, GET_DEFAULT_FORM_DATA } from './constants';
|
||||
import { DEFAULT_VALID_CONSUMERS, GET_DEFAULT_FORM_DATA } from './constants';
|
||||
import { RuleFormStateProvider } from './rule_form_state';
|
||||
import { useCreateRule } from '../common/hooks';
|
||||
import { RulePage } from './rule_page';
|
||||
|
@ -40,6 +40,8 @@ export interface CreateRuleFormProps {
|
|||
validConsumers?: RuleCreationValidConsumer[];
|
||||
filteredRuleTypes?: string[];
|
||||
shouldUseRuleProducer?: boolean;
|
||||
canShowConsumerSelection?: boolean;
|
||||
showMustacheAutocompleteSwitch?: boolean;
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
|
@ -47,16 +49,18 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
const {
|
||||
ruleTypeId,
|
||||
plugins,
|
||||
consumer = ALERTING_FEATURE_ID,
|
||||
consumer = 'alerts',
|
||||
multiConsumerSelection,
|
||||
validConsumers = DEFAULT_VALID_CONSUMERS,
|
||||
filteredRuleTypes = [],
|
||||
shouldUseRuleProducer = false,
|
||||
canShowConsumerSelection = true,
|
||||
showMustacheAutocompleteSwitch = false,
|
||||
returnUrl,
|
||||
} = props;
|
||||
|
||||
const { http, docLinks, notification, ruleTypeRegistry, i18n, theme } = plugins;
|
||||
const { toasts } = notification;
|
||||
const { http, docLinks, notifications, ruleTypeRegistry, i18n, theme } = plugins;
|
||||
const { toasts } = notifications;
|
||||
|
||||
const { mutate, isLoading: isSaving } = useCreateRule({
|
||||
http,
|
||||
|
@ -79,16 +83,25 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
},
|
||||
});
|
||||
|
||||
const { isInitialLoading, ruleType, ruleTypeModel, uiConfig, healthCheckError } =
|
||||
useLoadDependencies({
|
||||
http,
|
||||
toasts: notification.toasts,
|
||||
ruleTypeRegistry,
|
||||
ruleTypeId,
|
||||
consumer,
|
||||
validConsumers,
|
||||
filteredRuleTypes,
|
||||
});
|
||||
const {
|
||||
isInitialLoading,
|
||||
ruleType,
|
||||
ruleTypeModel,
|
||||
uiConfig,
|
||||
healthCheckError,
|
||||
connectors,
|
||||
connectorTypes,
|
||||
aadTemplateFields,
|
||||
} = useLoadDependencies({
|
||||
http,
|
||||
toasts: notifications.toasts,
|
||||
capabilities: plugins.application.capabilities,
|
||||
ruleTypeRegistry,
|
||||
ruleTypeId,
|
||||
consumer,
|
||||
validConsumers,
|
||||
filteredRuleTypes,
|
||||
});
|
||||
|
||||
const onSave = useCallback(
|
||||
(newFormData: RuleFormData) => {
|
||||
|
@ -101,8 +114,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
tags: newFormData.tags,
|
||||
params: newFormData.params,
|
||||
schedule: newFormData.schedule,
|
||||
// TODO: Will add actions in the actions PR
|
||||
actions: [],
|
||||
actions: newFormData.actions,
|
||||
notifyWhen: newFormData.notifyWhen,
|
||||
alertDelay: newFormData.alertDelay,
|
||||
},
|
||||
|
@ -151,12 +163,18 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
ruleType,
|
||||
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
|
||||
}),
|
||||
actions: [],
|
||||
}),
|
||||
plugins,
|
||||
connectors,
|
||||
connectorTypes,
|
||||
aadTemplateFields,
|
||||
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
|
||||
selectedRuleTypeModel: ruleTypeModel,
|
||||
selectedRuleType: ruleType,
|
||||
validConsumers,
|
||||
canShowConsumerSelection,
|
||||
showMustacheAutocompleteSwitch,
|
||||
multiConsumerSelection: getInitialMultiConsumer({
|
||||
multiConsumerSelection,
|
||||
validConsumers,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiLoadingElastic } from '@elastic/eui';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import type { RuleFormData, RuleFormPlugins } from './types';
|
||||
|
@ -17,6 +17,7 @@ import { RulePage } from './rule_page';
|
|||
import { RuleFormHealthCheckError } from './rule_form_errors/rule_form_health_check_error';
|
||||
import { useLoadDependencies } from './hooks/use_load_dependencies';
|
||||
import {
|
||||
RuleFormActionPermissionError,
|
||||
RuleFormCircuitBreakerError,
|
||||
RuleFormErrorPromptWrapper,
|
||||
RuleFormResolveRuleError,
|
||||
|
@ -28,13 +29,14 @@ import { parseRuleCircuitBreakerErrorMessage } from './utils';
|
|||
export interface EditRuleFormProps {
|
||||
id: string;
|
||||
plugins: RuleFormPlugins;
|
||||
showMustacheAutocompleteSwitch?: boolean;
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
export const EditRuleForm = (props: EditRuleFormProps) => {
|
||||
const { id, plugins, returnUrl } = props;
|
||||
const { http, notification, docLinks, ruleTypeRegistry, i18n, theme } = plugins;
|
||||
const { toasts } = notification;
|
||||
const { id, plugins, returnUrl, showMustacheAutocompleteSwitch = false } = props;
|
||||
const { http, notifications, docLinks, ruleTypeRegistry, i18n, theme, application } = plugins;
|
||||
const { toasts } = notifications;
|
||||
|
||||
const { mutate, isLoading: isSaving } = useUpdateRule({
|
||||
http,
|
||||
|
@ -57,13 +59,23 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
},
|
||||
});
|
||||
|
||||
const { isInitialLoading, ruleType, ruleTypeModel, uiConfig, healthCheckError, fetchedFormData } =
|
||||
useLoadDependencies({
|
||||
http,
|
||||
toasts: notification.toasts,
|
||||
ruleTypeRegistry,
|
||||
id,
|
||||
});
|
||||
const {
|
||||
isInitialLoading,
|
||||
ruleType,
|
||||
ruleTypeModel,
|
||||
uiConfig,
|
||||
healthCheckError,
|
||||
fetchedFormData,
|
||||
connectors,
|
||||
connectorTypes,
|
||||
aadTemplateFields,
|
||||
} = useLoadDependencies({
|
||||
http,
|
||||
toasts: notifications.toasts,
|
||||
capabilities: plugins.application.capabilities,
|
||||
ruleTypeRegistry,
|
||||
id,
|
||||
});
|
||||
|
||||
const onSave = useCallback(
|
||||
(newFormData: RuleFormData) => {
|
||||
|
@ -74,8 +86,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
tags: newFormData.tags,
|
||||
schedule: newFormData.schedule,
|
||||
params: newFormData.params,
|
||||
// TODO: Will add actions in the actions PR
|
||||
actions: [],
|
||||
actions: newFormData.actions,
|
||||
notifyWhen: newFormData.notifyWhen,
|
||||
alertDelay: newFormData.alertDelay,
|
||||
},
|
||||
|
@ -84,6 +95,18 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
[id, mutate]
|
||||
);
|
||||
|
||||
const canEditRule = useMemo(() => {
|
||||
if (!ruleType || !fetchedFormData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { consumer, actions } = fetchedFormData;
|
||||
const hasAllPrivilege = !!ruleType.authorizedConsumers[consumer]?.all;
|
||||
const canExecuteActions = !!application.capabilities.actions?.execute;
|
||||
|
||||
return hasAllPrivilege && (canExecuteActions || (!canExecuteActions && !actions.length));
|
||||
}, [ruleType, fetchedFormData, application]);
|
||||
|
||||
if (isInitialLoading) {
|
||||
return (
|
||||
<RuleFormErrorPromptWrapper hasBorder={false} hasShadow={false}>
|
||||
|
@ -92,14 +115,6 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (!ruleType || !ruleTypeModel) {
|
||||
return (
|
||||
<RuleFormErrorPromptWrapper hasBorder={false} hasShadow={false}>
|
||||
<RuleFormRuleTypeError />
|
||||
</RuleFormErrorPromptWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!fetchedFormData) {
|
||||
return (
|
||||
<RuleFormErrorPromptWrapper hasBorder={false} hasShadow={false}>
|
||||
|
@ -108,6 +123,14 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (!ruleType || !ruleTypeModel) {
|
||||
return (
|
||||
<RuleFormErrorPromptWrapper hasBorder={false} hasShadow={false}>
|
||||
<RuleFormRuleTypeError />
|
||||
</RuleFormErrorPromptWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (healthCheckError) {
|
||||
return (
|
||||
<RuleFormErrorPromptWrapper>
|
||||
|
@ -116,16 +139,28 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (!canEditRule) {
|
||||
return (
|
||||
<RuleFormErrorPromptWrapper hasBorder={false} hasShadow={false}>
|
||||
<RuleFormActionPermissionError />
|
||||
</RuleFormErrorPromptWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-test-subj="editRuleForm">
|
||||
<RuleFormStateProvider
|
||||
initialRuleFormState={{
|
||||
connectors,
|
||||
connectorTypes,
|
||||
aadTemplateFields,
|
||||
formData: fetchedFormData,
|
||||
id,
|
||||
plugins,
|
||||
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleTypeModel,
|
||||
showMustacheAutocompleteSwitch,
|
||||
}}
|
||||
>
|
||||
<RulePage isEdit={true} isSaving={isSaving} returnUrl={returnUrl} onSave={onSave} />
|
||||
|
|
|
@ -16,6 +16,7 @@ import type { ToastsStart } from '@kbn/core-notifications-browser';
|
|||
|
||||
import { useLoadDependencies } from './use_load_dependencies';
|
||||
import { RuleTypeRegistryContract } from '../../common';
|
||||
import { ApplicationStart } from '@kbn/core-application-browser';
|
||||
|
||||
jest.mock('../../common/hooks/use_load_ui_config', () => ({
|
||||
useLoadUiConfig: jest.fn(),
|
||||
|
@ -33,6 +34,18 @@ jest.mock('../../common/hooks/use_load_rule_types_query', () => ({
|
|||
useLoadRuleTypesQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/hooks/use_load_connectors', () => ({
|
||||
useLoadConnectors: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/hooks/use_load_connector_types', () => ({
|
||||
useLoadConnectorTypes: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/hooks/use_load_rule_type_aad_template_fields', () => ({
|
||||
useLoadRuleTypeAadTemplateField: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/get_authorized_rule_types', () => ({
|
||||
getAvailableRuleTypes: jest.fn(),
|
||||
}));
|
||||
|
@ -40,6 +53,11 @@ jest.mock('../utils/get_authorized_rule_types', () => ({
|
|||
const { useLoadUiConfig } = jest.requireMock('../../common/hooks/use_load_ui_config');
|
||||
const { useHealthCheck } = jest.requireMock('../../common/hooks/use_health_check');
|
||||
const { useResolveRule } = jest.requireMock('../../common/hooks/use_resolve_rule');
|
||||
const { useLoadConnectors } = jest.requireMock('../../common/hooks/use_load_connectors');
|
||||
const { useLoadConnectorTypes } = jest.requireMock('../../common/hooks/use_load_connector_types');
|
||||
const { useLoadRuleTypeAadTemplateField } = jest.requireMock(
|
||||
'../../common/hooks/use_load_rule_type_aad_template_fields'
|
||||
);
|
||||
const { useLoadRuleTypesQuery } = jest.requireMock('../../common/hooks/use_load_rule_types_query');
|
||||
const { getAvailableRuleTypes } = jest.requireMock('../utils/get_authorized_rule_types');
|
||||
|
||||
|
@ -141,6 +159,55 @@ getAvailableRuleTypes.mockReturnValue([
|
|||
},
|
||||
]);
|
||||
|
||||
const mockConnector = {
|
||||
id: 'test-connector',
|
||||
name: 'Test',
|
||||
connector_type_id: 'test',
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
is_missing_secrets: false,
|
||||
is_system_action: false,
|
||||
referenced_by_count: 0,
|
||||
secrets: {},
|
||||
config: {},
|
||||
};
|
||||
|
||||
const mockConnectorType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
enabled: true,
|
||||
enabled_in_config: true,
|
||||
enabled_in_license: true,
|
||||
supported_feature_ids: ['alerting'],
|
||||
minimum_license_required: 'basic',
|
||||
is_system_action_type: false,
|
||||
};
|
||||
|
||||
const mockAadTemplateField = {
|
||||
name: '@timestamp',
|
||||
deprecated: false,
|
||||
useWithTripleBracesInTemplates: false,
|
||||
usesPublicBaseUrl: false,
|
||||
};
|
||||
|
||||
useLoadConnectors.mockReturnValue({
|
||||
data: [mockConnector],
|
||||
isLoading: false,
|
||||
isInitialLoading: false,
|
||||
});
|
||||
|
||||
useLoadConnectorTypes.mockReturnValue({
|
||||
data: [mockConnectorType],
|
||||
isLoading: false,
|
||||
isInitialLoading: false,
|
||||
});
|
||||
|
||||
useLoadRuleTypeAadTemplateField.mockReturnValue({
|
||||
data: [mockAadTemplateField],
|
||||
isLoading: false,
|
||||
isInitialLoading: false,
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
|
@ -169,6 +236,13 @@ describe('useLoadDependencies', () => {
|
|||
http: httpMock as unknown as HttpStart,
|
||||
toasts: toastsMock as unknown as ToastsStart,
|
||||
ruleTypeRegistry: ruleTypeRegistryMock,
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
save: true,
|
||||
execute: true,
|
||||
},
|
||||
} as unknown as ApplicationStart['capabilities'],
|
||||
});
|
||||
},
|
||||
{ wrapper }
|
||||
|
@ -186,6 +260,9 @@ describe('useLoadDependencies', () => {
|
|||
uiConfig: uiConfigMock,
|
||||
healthCheckError: null,
|
||||
fetchedFormData: ruleMock,
|
||||
connectors: [mockConnector],
|
||||
connectorTypes: [mockConnectorType],
|
||||
aadTemplateFields: [mockAadTemplateField],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -197,6 +274,13 @@ describe('useLoadDependencies', () => {
|
|||
toasts: toastsMock as unknown as ToastsStart,
|
||||
ruleTypeRegistry: ruleTypeRegistryMock,
|
||||
filteredRuleTypes: ['test-rule-type'],
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
save: true,
|
||||
execute: true,
|
||||
},
|
||||
} as unknown as ApplicationStart['capabilities'],
|
||||
});
|
||||
},
|
||||
{ wrapper }
|
||||
|
@ -222,6 +306,13 @@ describe('useLoadDependencies', () => {
|
|||
ruleTypeRegistry: ruleTypeRegistryMock,
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
consumer: 'logs',
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
save: true,
|
||||
execute: true,
|
||||
},
|
||||
} as unknown as ApplicationStart['capabilities'],
|
||||
});
|
||||
},
|
||||
{ wrapper }
|
||||
|
@ -247,6 +338,13 @@ describe('useLoadDependencies', () => {
|
|||
toasts: toastsMock as unknown as ToastsStart,
|
||||
ruleTypeRegistry: ruleTypeRegistryMock,
|
||||
id: 'test-rule-id',
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
save: true,
|
||||
execute: true,
|
||||
},
|
||||
} as unknown as ApplicationStart['capabilities'],
|
||||
});
|
||||
},
|
||||
{ wrapper }
|
||||
|
@ -277,6 +375,13 @@ describe('useLoadDependencies', () => {
|
|||
ruleTypeRegistry: ruleTypeRegistryMock,
|
||||
ruleTypeId: '.index-threshold',
|
||||
consumer: 'stackAlerts',
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
save: true,
|
||||
execute: true,
|
||||
},
|
||||
} as unknown as ApplicationStart['capabilities'],
|
||||
});
|
||||
},
|
||||
{ wrapper }
|
||||
|
@ -304,6 +409,13 @@ describe('useLoadDependencies', () => {
|
|||
ruleTypeRegistry: ruleTypeRegistryMock,
|
||||
id: 'rule-id',
|
||||
consumer: 'stackAlerts',
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
save: true,
|
||||
execute: true,
|
||||
},
|
||||
} as unknown as ApplicationStart['capabilities'],
|
||||
});
|
||||
},
|
||||
{ wrapper }
|
||||
|
|
|
@ -9,21 +9,26 @@
|
|||
|
||||
import { HttpStart } from '@kbn/core-http-browser';
|
||||
import type { ToastsStart } from '@kbn/core-notifications-browser';
|
||||
import { ApplicationStart } from '@kbn/core-application-browser';
|
||||
import { RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
useHealthCheck,
|
||||
useLoadConnectors,
|
||||
useLoadConnectorTypes,
|
||||
useLoadRuleTypesQuery,
|
||||
useLoadUiConfig,
|
||||
useResolveRule,
|
||||
} from '../../common/hooks';
|
||||
import { getAvailableRuleTypes } from '../utils';
|
||||
import { RuleTypeRegistryContract } from '../../common';
|
||||
import { useLoadRuleTypeAadTemplateField } from '../../common/hooks/use_load_rule_type_aad_template_fields';
|
||||
|
||||
export interface UseLoadDependencies {
|
||||
http: HttpStart;
|
||||
toasts: ToastsStart;
|
||||
ruleTypeRegistry: RuleTypeRegistryContract;
|
||||
capabilities: ApplicationStart['capabilities'];
|
||||
consumer?: string;
|
||||
id?: string;
|
||||
ruleTypeId?: string;
|
||||
|
@ -40,9 +45,12 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
validConsumers,
|
||||
id,
|
||||
ruleTypeId,
|
||||
capabilities,
|
||||
filteredRuleTypes = [],
|
||||
} = props;
|
||||
|
||||
const canReadConnectors = !!capabilities.actions?.show;
|
||||
|
||||
const {
|
||||
data: uiConfig,
|
||||
isLoading: isLoadingUiConfig,
|
||||
|
@ -73,10 +81,41 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
filteredRuleTypes,
|
||||
});
|
||||
|
||||
const {
|
||||
data: connectors = [],
|
||||
isLoading: isLoadingConnectors,
|
||||
isInitialLoading: isInitialLoadingConnectors,
|
||||
} = useLoadConnectors({
|
||||
http,
|
||||
includeSystemActions: true,
|
||||
enabled: canReadConnectors,
|
||||
});
|
||||
|
||||
const computedRuleTypeId = useMemo(() => {
|
||||
return fetchedFormData?.ruleTypeId || ruleTypeId;
|
||||
}, [fetchedFormData, ruleTypeId]);
|
||||
|
||||
// Fetching Action related dependencies
|
||||
const {
|
||||
data: connectorTypes = [],
|
||||
isLoading: isLoadingConnectorTypes,
|
||||
isInitialLoading: isInitialLoadingConnectorTypes,
|
||||
} = useLoadConnectorTypes({
|
||||
http,
|
||||
includeSystemActions: true,
|
||||
enabled: canReadConnectors,
|
||||
});
|
||||
|
||||
const {
|
||||
data: aadTemplateFields,
|
||||
isLoading: isLoadingAadtemplateFields,
|
||||
isInitialLoading: isInitialLoadingAadTemplateField,
|
||||
} = useLoadRuleTypeAadTemplateField({
|
||||
http,
|
||||
ruleTypeId: computedRuleTypeId,
|
||||
enabled: !!computedRuleTypeId && canReadConnectors,
|
||||
});
|
||||
|
||||
const authorizedRuleTypeItems = useMemo(() => {
|
||||
const computedConsumer = consumer || fetchedFormData?.consumer;
|
||||
if (!computedConsumer) {
|
||||
|
@ -99,21 +138,61 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
}, [authorizedRuleTypeItems, computedRuleTypeId]);
|
||||
|
||||
const isLoading = useMemo(() => {
|
||||
// Create Mode
|
||||
if (id === undefined) {
|
||||
return isLoadingUiConfig || isLoadingHealthCheck || isLoadingRuleTypes;
|
||||
return (
|
||||
isLoadingUiConfig ||
|
||||
isLoadingHealthCheck ||
|
||||
isLoadingRuleTypes ||
|
||||
isLoadingConnectors ||
|
||||
isLoadingConnectorTypes ||
|
||||
isLoadingAadtemplateFields
|
||||
);
|
||||
}
|
||||
return isLoadingUiConfig || isLoadingHealthCheck || isLoadingRule || isLoadingRuleTypes;
|
||||
}, [id, isLoadingUiConfig, isLoadingHealthCheck, isLoadingRule, isLoadingRuleTypes]);
|
||||
|
||||
// Edit Mode
|
||||
return (
|
||||
isLoadingUiConfig ||
|
||||
isLoadingHealthCheck ||
|
||||
isLoadingRule ||
|
||||
isLoadingRuleTypes ||
|
||||
isLoadingConnectors ||
|
||||
isLoadingConnectorTypes ||
|
||||
isLoadingAadtemplateFields
|
||||
);
|
||||
}, [
|
||||
id,
|
||||
isLoadingUiConfig,
|
||||
isLoadingHealthCheck,
|
||||
isLoadingRule,
|
||||
isLoadingRuleTypes,
|
||||
isLoadingConnectors,
|
||||
isLoadingConnectorTypes,
|
||||
isLoadingAadtemplateFields,
|
||||
]);
|
||||
|
||||
const isInitialLoading = useMemo(() => {
|
||||
// Create Mode
|
||||
if (id === undefined) {
|
||||
return isInitialLoadingUiConfig || isInitialLoadingHealthCheck || isInitialLoadingRuleTypes;
|
||||
return (
|
||||
isInitialLoadingUiConfig ||
|
||||
isInitialLoadingHealthCheck ||
|
||||
isInitialLoadingRuleTypes ||
|
||||
isInitialLoadingConnectors ||
|
||||
isInitialLoadingConnectorTypes ||
|
||||
isInitialLoadingAadTemplateField
|
||||
);
|
||||
}
|
||||
|
||||
// Edit Mode
|
||||
return (
|
||||
isInitialLoadingUiConfig ||
|
||||
isInitialLoadingHealthCheck ||
|
||||
isInitialLoadingRule ||
|
||||
isInitialLoadingRuleTypes
|
||||
isInitialLoadingRuleTypes ||
|
||||
isInitialLoadingConnectors ||
|
||||
isInitialLoadingConnectorTypes ||
|
||||
isInitialLoadingAadTemplateField
|
||||
);
|
||||
}, [
|
||||
id,
|
||||
|
@ -121,6 +200,9 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
isInitialLoadingHealthCheck,
|
||||
isInitialLoadingRule,
|
||||
isInitialLoadingRuleTypes,
|
||||
isInitialLoadingConnectors,
|
||||
isInitialLoadingConnectorTypes,
|
||||
isInitialLoadingAadTemplateField,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
@ -131,5 +213,8 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
uiConfig,
|
||||
healthCheckError,
|
||||
fetchedFormData,
|
||||
connectors,
|
||||
connectorTypes,
|
||||
aadTemplateFields,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,27 +8,264 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { RuleActions } from './rule_actions';
|
||||
import {
|
||||
getActionType,
|
||||
getAction,
|
||||
getSystemAction,
|
||||
getConnector,
|
||||
getActionTypeModel,
|
||||
} from '../../common/test_utils/actions_test_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ActionConnector, ActionTypeModel } from '../../common/types';
|
||||
import { RuleActionsItemProps } from './rule_actions_item';
|
||||
import { TypeRegistry } from '../../common/type_registry';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_system_actions_item', () => ({
|
||||
RuleActionsSystemActionsItem: ({ action, producerId }: RuleActionsItemProps) => (
|
||||
<div>
|
||||
RuleActionsSystemActionsItem
|
||||
<div>
|
||||
{action.id} producerId: {producerId}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_item', () => ({
|
||||
RuleActionsItem: ({ action, producerId }: RuleActionsItemProps) => (
|
||||
<div>
|
||||
RuleActionsItem
|
||||
<div>
|
||||
{action.id} producerId: {producerId}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_connectors_modal', () => ({
|
||||
RuleActionsConnectorsModal: ({
|
||||
onSelectConnector,
|
||||
}: {
|
||||
onSelectConnector: (connector: ActionConnector) => void;
|
||||
}) => (
|
||||
<div>
|
||||
RuleActionsConnectorsModal
|
||||
<button
|
||||
onClick={() =>
|
||||
onSelectConnector({
|
||||
id: 'connector-1',
|
||||
secrets: { secret: 'secret' },
|
||||
actionTypeId: 'actionType-1',
|
||||
name: 'connector-1',
|
||||
config: { config: 'config-1' },
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
isDeprecated: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
select connector
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/hooks', () => ({
|
||||
useLoadConnectors: jest.fn(),
|
||||
useLoadConnectorTypes: jest.fn(),
|
||||
useLoadRuleTypeAadTemplateField: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockValidate = jest.fn().mockResolvedValue({
|
||||
errors: {},
|
||||
});
|
||||
|
||||
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
|
||||
const { useLoadConnectors, useLoadConnectorTypes, useLoadRuleTypeAadTemplateField } =
|
||||
jest.requireMock('../../common/hooks');
|
||||
|
||||
const mockConnectors = [getConnector('1')];
|
||||
const mockConnectorTypes = [
|
||||
getActionType('1'),
|
||||
getActionType('2'),
|
||||
getActionType('3', { isSystemActionType: true }),
|
||||
];
|
||||
|
||||
const mockActions = [getAction('1'), getAction('2')];
|
||||
const mockSystemActions = [getSystemAction('3')];
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
describe('Rule actions', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
describe('ruleActions', () => {
|
||||
beforeEach(() => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('1', {
|
||||
id: 'actionType-1',
|
||||
validateParams: mockValidate,
|
||||
})
|
||||
);
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('2', {
|
||||
id: 'actionType-2',
|
||||
validateParams: mockValidate,
|
||||
})
|
||||
);
|
||||
|
||||
useLoadConnectors.mockReturnValue({
|
||||
data: mockConnectors,
|
||||
isInitialLoading: false,
|
||||
});
|
||||
useLoadConnectorTypes.mockReturnValue({
|
||||
data: mockConnectorTypes,
|
||||
isInitialLoading: false,
|
||||
});
|
||||
useLoadRuleTypeAadTemplateField.mockReturnValue({
|
||||
data: {},
|
||||
isInitialLoading: false,
|
||||
});
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
http,
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [...mockActions, ...mockSystemActions],
|
||||
consumer: 'stackAlerts',
|
||||
},
|
||||
selectedRuleType: {
|
||||
id: 'selectedRuleTypeId',
|
||||
defaultActionGroupId: 'test',
|
||||
producer: 'stackAlerts',
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockConnectorTypes,
|
||||
aadTemplateFields: [],
|
||||
});
|
||||
useRuleFormDispatch.mockReturnValue(mockOnChange);
|
||||
});
|
||||
|
||||
test('Renders correctly', () => {
|
||||
render(<RuleActions onClick={mockOnChange} />);
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders correctly', () => {
|
||||
render(<RuleActions />);
|
||||
|
||||
expect(screen.getByTestId('ruleActions')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ruleActionsAddActionButton')).toBeInTheDocument();
|
||||
expect(screen.queryByText('RuleActionsConnectorsModal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Calls onChange when button is click', () => {
|
||||
render(<RuleActions onClick={mockOnChange} />);
|
||||
test('renders actions correctly', () => {
|
||||
render(<RuleActions />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('ruleActionsAddActionButton'));
|
||||
expect(screen.getAllByText('RuleActionsItem').length).toEqual(2);
|
||||
expect(screen.getAllByText('RuleActionsSystemActionsItem').length).toEqual(1);
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
test('should show no actions if none are selected', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
http,
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
consumer: 'stackAlerts',
|
||||
},
|
||||
selectedRuleType: {
|
||||
id: 'selectedRuleTypeId',
|
||||
defaultActionGroupId: 'test',
|
||||
producer: 'stackAlerts',
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockConnectorTypes,
|
||||
aadTemplateFields: [],
|
||||
});
|
||||
|
||||
render(<RuleActions />);
|
||||
expect(screen.queryAllByText('RuleActionsItem').length).toEqual(0);
|
||||
expect(screen.queryAllByText('RuleActionsSystemActionsItem').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should be able to open and close the connector modal', async () => {
|
||||
render(<RuleActions />);
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsAddActionButton'));
|
||||
expect(screen.getByText('RuleActionsConnectorsModal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onSelectConnector with the correct parameters', async () => {
|
||||
render(<RuleActions />);
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsAddActionButton'));
|
||||
expect(screen.getByText('RuleActionsConnectorsModal')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('select connector'));
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
actionTypeId: 'actionType-1',
|
||||
frequency: { notifyWhen: 'onActionGroupChange', summary: false, throttle: null },
|
||||
group: 'test',
|
||||
id: 'connector-1',
|
||||
params: {},
|
||||
uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
||||
},
|
||||
type: 'addAction',
|
||||
});
|
||||
|
||||
expect(screen.queryByText('RuleActionsConnectorsModal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should use the rule producer ID if it is not a multi-consumer rule', async () => {
|
||||
render(<RuleActions />);
|
||||
|
||||
expect(await screen.findByText('action-1 producerId: stackAlerts')).toBeInTheDocument();
|
||||
expect(await screen.findByText('action-1 producerId: stackAlerts')).toBeInTheDocument();
|
||||
expect(await screen.findByText('system-action-3 producerId: stackAlerts')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should use the rules consumer if the rule is a multi-consumer rule', async () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
http,
|
||||
},
|
||||
formData: {
|
||||
actions: [...mockActions, ...mockSystemActions],
|
||||
consumer: 'logs',
|
||||
},
|
||||
selectedRuleType: {
|
||||
id: 'observability.rules.custom_threshold',
|
||||
defaultActionGroupId: 'test',
|
||||
producer: 'stackAlerts',
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: [
|
||||
getActionType('1'),
|
||||
getActionType('2'),
|
||||
getActionType('3', { isSystemActionType: true }),
|
||||
],
|
||||
aadTemplateFields: [],
|
||||
});
|
||||
|
||||
render(<RuleActions />);
|
||||
|
||||
expect(await screen.findByText('action-1 producerId: logs')).toBeInTheDocument();
|
||||
expect(await screen.findByText('action-1 producerId: logs')).toBeInTheDocument();
|
||||
expect(await screen.findByText('system-action-3 producerId: logs')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,26 +7,121 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiButton, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { RuleSystemAction } from '@kbn/alerting-types';
|
||||
import { ADD_ACTION_TEXT } from '../translations';
|
||||
import { RuleActionsConnectorsModal } from './rule_actions_connectors_modal';
|
||||
import { useRuleFormDispatch, useRuleFormState } from '../hooks';
|
||||
import { ActionConnector, RuleAction, RuleFormParamsErrors } from '../../common/types';
|
||||
import { DEFAULT_FREQUENCY, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants';
|
||||
import { RuleActionsItem } from './rule_actions_item';
|
||||
import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item';
|
||||
|
||||
export interface RuleActionsProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
export const RuleActions = () => {
|
||||
const [isConnectorModalOpen, setIsConnectorModalOpen] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
formData: { actions, consumer },
|
||||
plugins: { actionTypeRegistry },
|
||||
multiConsumerSelection,
|
||||
selectedRuleType,
|
||||
connectorTypes,
|
||||
} = useRuleFormState();
|
||||
|
||||
const dispatch = useRuleFormDispatch();
|
||||
|
||||
const onModalOpen = useCallback(() => {
|
||||
setIsConnectorModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const onModalClose = useCallback(() => {
|
||||
setIsConnectorModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const onSelectConnector = useCallback(
|
||||
async (connector: ActionConnector) => {
|
||||
const { id, actionTypeId } = connector;
|
||||
const uuid = uuidv4();
|
||||
const params = {};
|
||||
|
||||
dispatch({
|
||||
type: 'addAction',
|
||||
payload: {
|
||||
id,
|
||||
actionTypeId,
|
||||
uuid,
|
||||
params,
|
||||
group: selectedRuleType.defaultActionGroupId,
|
||||
frequency: DEFAULT_FREQUENCY,
|
||||
},
|
||||
});
|
||||
|
||||
const res: { errors: RuleFormParamsErrors } = await actionTypeRegistry
|
||||
.get(actionTypeId)
|
||||
?.validateParams(params);
|
||||
|
||||
dispatch({
|
||||
type: 'setActionParamsError',
|
||||
payload: {
|
||||
uuid,
|
||||
errors: res.errors,
|
||||
},
|
||||
});
|
||||
|
||||
onModalClose();
|
||||
},
|
||||
[dispatch, onModalClose, selectedRuleType, actionTypeRegistry]
|
||||
);
|
||||
|
||||
const producerId = useMemo(() => {
|
||||
if (MULTI_CONSUMER_RULE_TYPE_IDS.includes(selectedRuleType.id)) {
|
||||
return multiConsumerSelection || consumer;
|
||||
}
|
||||
return selectedRuleType.producer;
|
||||
}, [consumer, multiConsumerSelection, selectedRuleType]);
|
||||
|
||||
export const RuleActions = (props: RuleActionsProps) => {
|
||||
const { onClick } = props;
|
||||
return (
|
||||
<div data-test-subj="ruleActions">
|
||||
<>
|
||||
<EuiFlexGroup data-test-subj="ruleActions" direction="column">
|
||||
{actions.map((action, index) => {
|
||||
const isSystemAction = connectorTypes.some((connectorType) => {
|
||||
return connectorType.id === action.actionTypeId && connectorType.isSystemActionType;
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexItem key={action.uuid}>
|
||||
{isSystemAction && (
|
||||
<RuleActionsSystemActionsItem
|
||||
action={action as RuleSystemAction}
|
||||
index={index}
|
||||
producerId={producerId}
|
||||
/>
|
||||
)}
|
||||
{!isSystemAction && (
|
||||
<RuleActionsItem
|
||||
action={action as RuleAction}
|
||||
index={index}
|
||||
producerId={producerId}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
data-test-subj="ruleActionsAddActionButton"
|
||||
iconType="push"
|
||||
iconSide="left"
|
||||
onClick={onClick}
|
||||
data-test-subj="ruleActionsAddActionButton"
|
||||
onClick={onModalOpen}
|
||||
>
|
||||
{ADD_ACTION_TEXT}
|
||||
</EuiButton>
|
||||
</div>
|
||||
{isConnectorModalOpen && (
|
||||
<RuleActionsConnectorsModal onClose={onModalClose} onSelectConnector={onSelectConnector} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { RuleActionsAlertsFilter } from './rule_actions_alerts_filter';
|
||||
import type { AlertsSearchBarProps } from '../../alerts_search_bar';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { getAction } from '../../common/test_utils/actions_test_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../alerts_search_bar', () => ({
|
||||
AlertsSearchBar: ({ onFiltersUpdated, onQueryChange, onQuerySubmit }: AlertsSearchBarProps) => (
|
||||
<div>
|
||||
AlertsSearchBar
|
||||
<button
|
||||
onClick={() =>
|
||||
onFiltersUpdated?.([
|
||||
{
|
||||
$state: { store: 'appState' as FilterStateStore },
|
||||
meta: {},
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
Update Filter
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onQueryChange?.({
|
||||
dateRange: { from: 'now', to: 'now' },
|
||||
query: 'onQueryChange',
|
||||
})
|
||||
}
|
||||
>
|
||||
Update Query
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onQuerySubmit?.({
|
||||
dateRange: { from: 'now', to: 'now' },
|
||||
query: 'onQuerySubmit',
|
||||
})
|
||||
}
|
||||
>
|
||||
Submit Query
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const { useRuleFormState } = jest.requireMock('../hooks');
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
describe('ruleActionsAlertsFilter', () => {
|
||||
beforeEach(() => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
http,
|
||||
notifications: {
|
||||
toasts: {},
|
||||
},
|
||||
unifiedSearch: {
|
||||
ui: {
|
||||
SearchBar: {},
|
||||
},
|
||||
},
|
||||
dataViews: {},
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
consumer: 'stackAlerts',
|
||||
},
|
||||
selectedRuleType: {
|
||||
id: 'selectedRuleTypeId',
|
||||
defaultActionGroupId: 'test',
|
||||
producer: 'stackAlerts',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render correctly', () => {
|
||||
render(
|
||||
<RuleActionsAlertsFilter
|
||||
action={getAction('1')}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('alertsFilterQueryToggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow for toggling on of query', async () => {
|
||||
render(
|
||||
<RuleActionsAlertsFilter
|
||||
action={getAction('1')}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('alertsFilterQueryToggle'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith({ filters: [], kql: '' });
|
||||
});
|
||||
|
||||
test('should allow for toggling off of query', async () => {
|
||||
render(
|
||||
<RuleActionsAlertsFilter
|
||||
action={getAction('1', {
|
||||
alertsFilter: {
|
||||
query: {
|
||||
kql: 'test',
|
||||
filters: [],
|
||||
},
|
||||
},
|
||||
})}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('alertsFilterQueryToggle'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith(undefined);
|
||||
});
|
||||
|
||||
test('should allow for changing query', async () => {
|
||||
render(
|
||||
<RuleActionsAlertsFilter
|
||||
action={getAction('1', {
|
||||
alertsFilter: {
|
||||
query: {
|
||||
kql: 'test',
|
||||
filters: [],
|
||||
},
|
||||
},
|
||||
})}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Update Filter'));
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith({
|
||||
filters: [{ $state: { store: 'appState' }, meta: {} }],
|
||||
kql: 'test',
|
||||
});
|
||||
await userEvent.click(screen.getByText('Update Query'));
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith({
|
||||
filters: [{ $state: { store: 'appState' }, meta: {} }],
|
||||
kql: 'onQueryChange',
|
||||
});
|
||||
await userEvent.click(screen.getByText('Submit Query'));
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith({
|
||||
filters: [{ $state: { store: 'appState' }, meta: {} }],
|
||||
kql: 'onQuerySubmit',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,35 +1,61 @@
|
|||
/*
|
||||
* 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.
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSwitch, EuiSpacer } from '@elastic/eui';
|
||||
import { AlertsFilter } from '@kbn/alerting-plugin/common';
|
||||
import type { AlertsFilter } from '@kbn/alerting-types';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { AlertsSearchBar, AlertsSearchBarProps } from '../alerts_search_bar';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
import { RuleAction } from '../../common';
|
||||
import { RuleFormPlugins } from '../types';
|
||||
import { AlertsSearchBar, AlertsSearchBarProps } from '../../alerts_search_bar';
|
||||
|
||||
interface ActionAlertsFilterQueryProps {
|
||||
state?: AlertsFilter['query'];
|
||||
const DEFAULT_QUERY = { kql: '', filters: [] };
|
||||
|
||||
export interface RuleActionsAlertsFilterProps {
|
||||
action: RuleAction;
|
||||
onChange: (update?: AlertsFilter['query']) => void;
|
||||
appName: string;
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeId?: string;
|
||||
plugins?: {
|
||||
http: RuleFormPlugins['http'];
|
||||
notifications: RuleFormPlugins['notifications'];
|
||||
unifiedSearch: RuleFormPlugins['unifiedSearch'];
|
||||
dataViews: RuleFormPlugins['dataViews'];
|
||||
};
|
||||
}
|
||||
|
||||
export const ActionAlertsFilterQuery: React.FC<ActionAlertsFilterQueryProps> = ({
|
||||
state,
|
||||
export const RuleActionsAlertsFilter = ({
|
||||
action,
|
||||
onChange,
|
||||
appName,
|
||||
featureIds,
|
||||
ruleTypeId,
|
||||
}) => {
|
||||
const [query, setQuery] = useState(state ?? { kql: '', filters: [] });
|
||||
plugins: propsPlugins,
|
||||
}: RuleActionsAlertsFilterProps) => {
|
||||
const { plugins } = useRuleFormState();
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
unifiedSearch,
|
||||
dataViews,
|
||||
} = propsPlugins || plugins;
|
||||
|
||||
const [query, setQuery] = useState(action.alertsFilter?.query ?? DEFAULT_QUERY);
|
||||
|
||||
const state = useMemo(() => {
|
||||
return action.alertsFilter?.query;
|
||||
}, [action]);
|
||||
|
||||
const queryEnabled = useMemo(() => Boolean(state), [state]);
|
||||
|
||||
|
@ -66,7 +92,7 @@ export const ActionAlertsFilterQuery: React.FC<ActionAlertsFilterQueryProps> = (
|
|||
<>
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryToggleLabel',
|
||||
'alertsUIShared.ruleActionsAlertsFilter.ActionAlertsFilterQueryToggleLabel',
|
||||
{
|
||||
defaultMessage: 'If alert matches a query',
|
||||
}
|
||||
|
@ -79,6 +105,10 @@ export const ActionAlertsFilterQuery: React.FC<ActionAlertsFilterQueryProps> = (
|
|||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<AlertsSearchBar
|
||||
http={http}
|
||||
toasts={toasts}
|
||||
unifiedSearchBar={unifiedSearch.ui.SearchBar}
|
||||
dataViewsService={dataViews}
|
||||
appName={appName}
|
||||
featureIds={featureIds}
|
||||
ruleTypeId={ruleTypeId}
|
||||
|
@ -93,7 +123,7 @@ export const ActionAlertsFilterQuery: React.FC<ActionAlertsFilterQueryProps> = (
|
|||
showDatePicker={false}
|
||||
showSubmitButton={false}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryPlaceholder',
|
||||
'alertsUIShared.ruleActionsAlertsFilter.ActionAlertsFilterQueryPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Filter alerts using KQL syntax',
|
||||
}
|
|
@ -14,12 +14,13 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
|||
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import { RuleActionsAlertsFilterTimeframe } from './rule_actions_alerts_filter_timeframe';
|
||||
import { AlertsFilterTimeframe } from '@kbn/alerting-types';
|
||||
import { getAction } from '../../common/test_utils/actions_test_utils';
|
||||
|
||||
describe('ruleActionsAlertsFilterTimeframe', () => {
|
||||
async function setup(timeframe?: AlertsFilterTimeframe) {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleActionsAlertsFilterTimeframe
|
||||
state={timeframe}
|
||||
action={getAction('1', { alertsFilter: { timeframe } })}
|
||||
settings={
|
||||
{
|
||||
client: {
|
||||
|
|
|
@ -24,9 +24,10 @@ import {
|
|||
import deepEqual from 'fast-deep-equal';
|
||||
import { ISO_WEEKDAYS, type IsoWeekday, type AlertsFilterTimeframe } from '@kbn/alerting-types';
|
||||
import { I18N_WEEKDAY_OPTIONS_DDD } from '../../common/constants';
|
||||
import { RuleAction } from '../../common';
|
||||
|
||||
interface RuleActionsAlertsFilterTimeframeProps {
|
||||
state?: AlertsFilterTimeframe;
|
||||
export interface RuleActionsAlertsFilterTimeframeProps {
|
||||
action: RuleAction;
|
||||
settings: SettingsStart;
|
||||
onChange: (update?: AlertsFilterTimeframe) => void;
|
||||
}
|
||||
|
@ -76,29 +77,30 @@ const useTimeFormat = (settings: SettingsStart) => {
|
|||
};
|
||||
|
||||
export const RuleActionsAlertsFilterTimeframe: React.FC<RuleActionsAlertsFilterTimeframeProps> = ({
|
||||
state,
|
||||
action,
|
||||
settings,
|
||||
onChange,
|
||||
}) => {
|
||||
const actionTimeFrame = action.alertsFilter?.timeframe;
|
||||
const timeFormat = useTimeFormat(settings);
|
||||
const [timeframe, setTimeframe] = useTimeframe({
|
||||
initialTimeframe: state,
|
||||
initialTimeframe: actionTimeFrame,
|
||||
settings,
|
||||
});
|
||||
const [selectedTimezone, setSelectedTimezone] = useState([{ label: timeframe.timezone }]);
|
||||
|
||||
const timeframeEnabled = useMemo(() => Boolean(state), [state]);
|
||||
const timeframeEnabled = useMemo(() => Boolean(actionTimeFrame), [actionTimeFrame]);
|
||||
|
||||
const weekdayOptions = useSortedWeekdayOptions(settings);
|
||||
|
||||
useEffect(() => {
|
||||
const nextState = timeframeEnabled ? timeframe : undefined;
|
||||
if (!deepEqual(state, nextState)) onChange(nextState);
|
||||
}, [timeframeEnabled, timeframe, state, onChange]);
|
||||
if (!deepEqual(actionTimeFrame, nextState)) onChange(nextState);
|
||||
}, [timeframeEnabled, timeframe, actionTimeFrame, onChange]);
|
||||
|
||||
const toggleTimeframe = useCallback(
|
||||
() => onChange(state ? undefined : timeframe),
|
||||
[state, timeframe, onChange]
|
||||
() => onChange(actionTimeFrame ? undefined : timeframe),
|
||||
[actionTimeFrame, timeframe, onChange]
|
||||
);
|
||||
const updateTimeframe = useCallback(
|
||||
(update: Partial<AlertsFilterTimeframe>) => {
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { RuleActionsConnectorsModal } from './rule_actions_connectors_modal';
|
||||
import { ActionConnector, ActionTypeModel } from '../../common';
|
||||
import { ActionType } from '@kbn/actions-types';
|
||||
import { TypeRegistry } from '../../common/type_registry';
|
||||
import {
|
||||
getActionType,
|
||||
getActionTypeModel,
|
||||
getConnector,
|
||||
} from '../../common/test_utils/actions_test_utils';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
|
||||
|
||||
const mockConnectors: ActionConnector[] = [getConnector('1'), getConnector('2')];
|
||||
|
||||
const mockActionTypes: ActionType[] = [getActionType('1'), getActionType('2')];
|
||||
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
const mockOnSelectConnector = jest.fn();
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
describe('ruleActionsConnectorsModal', () => {
|
||||
beforeEach(() => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
actionTypeRegistry.register(getActionTypeModel('2', { id: 'actionType-2' }));
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockActionTypes,
|
||||
aadTemplateFields: [],
|
||||
});
|
||||
useRuleFormDispatch.mockReturnValue(mockOnChange);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders correctly', () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
expect(screen.getByTestId('ruleActionsConnectorsModal'));
|
||||
});
|
||||
|
||||
test('should render connectors and filters', () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
expect(screen.getByText('connector-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('connector-2')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('ruleActionsConnectorsModalSearch')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('ruleActionsConnectorsModalFilterButton').length).toEqual(3);
|
||||
|
||||
const filterButtonGroup = screen.getByTestId('ruleActionsConnectorsModalFilterButtonGroup');
|
||||
expect(within(filterButtonGroup).getByText('actionType: 1')).toBeInTheDocument();
|
||||
expect(within(filterButtonGroup).getByText('actionType: 2')).toBeInTheDocument();
|
||||
expect(within(filterButtonGroup).getByText('All')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow for searching of connectors', async () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
// Type first connector
|
||||
await userEvent.type(screen.getByTestId('ruleActionsConnectorsModalSearch'), 'connector-1');
|
||||
expect(screen.getAllByTestId('ruleActionsConnectorsModalCard').length).toEqual(1);
|
||||
expect(screen.getByText('connector-1')).toBeInTheDocument();
|
||||
|
||||
// Clear
|
||||
await userEvent.clear(screen.getByTestId('ruleActionsConnectorsModalSearch'));
|
||||
|
||||
// Type second connector
|
||||
await userEvent.type(screen.getByTestId('ruleActionsConnectorsModalSearch'), 'actionType: 2');
|
||||
expect(screen.getAllByTestId('ruleActionsConnectorsModalCard').length).toEqual(1);
|
||||
expect(screen.getByText('connector-2')).toBeInTheDocument();
|
||||
|
||||
// Clear
|
||||
await userEvent.clear(screen.getByTestId('ruleActionsConnectorsModalSearch'));
|
||||
|
||||
// Type a connector that doesn't exist
|
||||
await userEvent.type(screen.getByTestId('ruleActionsConnectorsModalSearch'), 'doesntexist');
|
||||
expect(screen.getByTestId('ruleActionsConnectorsModalEmpty')).toBeInTheDocument();
|
||||
|
||||
// Clear
|
||||
await userEvent.click(screen.getByTestId('ruleActionsConnectorsModalClearFiltersButton'));
|
||||
expect(screen.getAllByTestId('ruleActionsConnectorsModalCard').length).toEqual(2);
|
||||
});
|
||||
|
||||
test('should allow for filtering of connectors', async () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
const filterButtonGroup = screen.getByTestId('ruleActionsConnectorsModalFilterButtonGroup');
|
||||
|
||||
await userEvent.click(within(filterButtonGroup).getByText('actionType: 1'));
|
||||
expect(screen.getAllByTestId('ruleActionsConnectorsModalCard').length).toEqual(1);
|
||||
expect(screen.getByText('connector-1')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(within(filterButtonGroup).getByText('actionType: 2'));
|
||||
expect(screen.getByText('connector-2')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('ruleActionsConnectorsModalCard').length).toEqual(1);
|
||||
|
||||
await userEvent.click(within(filterButtonGroup).getByText('All'));
|
||||
expect(screen.getAllByTestId('ruleActionsConnectorsModalCard').length).toEqual(2);
|
||||
});
|
||||
|
||||
test('should call onSelectConnector when connector is clicked', async () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('connector-1'));
|
||||
expect(mockOnSelectConnector).toHaveBeenLastCalledWith({
|
||||
actionTypeId: 'actionType-1',
|
||||
config: { config: 'config-1' },
|
||||
id: 'connector-1',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
name: 'connector-1',
|
||||
secrets: { secret: 'secret' },
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText('connector-2'));
|
||||
expect(mockOnSelectConnector).toHaveBeenLastCalledWith({
|
||||
actionTypeId: 'actionType-2',
|
||||
config: { config: 'config-2' },
|
||||
id: 'connector-2',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
name: 'connector-2',
|
||||
secrets: { secret: 'secret' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render connector if action type doesnt exist', () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
expect(screen.queryByText('connector2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render connector if hideInUi is true', () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
actionTypeRegistry.register(getActionTypeModel('2', { id: 'actionType-2', hideInUi: true }));
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockActionTypes,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
expect(screen.queryByText('connector2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render connector if actionsParamsField doesnt exist', () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('2', {
|
||||
id: 'actionType-2',
|
||||
actionParamsFields: null as unknown as React.LazyExoticComponent<any>,
|
||||
})
|
||||
);
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockActionTypes,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
expect(screen.queryByText('connector-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render connector if the action type is not enabled', () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
actionTypeRegistry.register(getActionTypeModel('2', { id: 'actionType-2' }));
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: [getActionType('1'), getActionType('2', { enabledInConfig: false })],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
expect(screen.queryByText('connector-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render connector if the action is not enabled but its a preconfigured connector', () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
actionTypeRegistry.register(getActionTypeModel('2', { id: 'actionType-2' }));
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
connectors: [getConnector('1'), getConnector('2', { isPreconfigured: true })],
|
||||
connectorTypes: [getActionType('1'), getActionType('2', { enabledInConfig: false })],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
expect(screen.getByText('connector-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should disable connector if it fails license check', () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
actionTypeRegistry.register(getActionTypeModel('2', { id: 'actionType-2' }));
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: [getActionType('1'), getActionType('2', { enabledInLicense: false })],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
expect(screen.getByText('connector-2')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should disable connector if its a selected system action', () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('2', { isSystemActionType: true, id: 'actionType-2' })
|
||||
);
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [{ actionTypeId: 'actionType-2' }],
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: [getActionType('1'), getActionType('2', { isSystemActionType: true })],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
expect(screen.getByText('connector-2')).toBeDisabled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,348 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo, Suspense } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiFieldSearch,
|
||||
EuiFacetButton,
|
||||
EuiModalBody,
|
||||
EuiHorizontalRule,
|
||||
EuiModalHeaderTitle,
|
||||
useEuiTheme,
|
||||
EuiEmptyPrompt,
|
||||
EuiFacetGroup,
|
||||
EuiCard,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
useCurrentEuiBreakpoint,
|
||||
EuiButton,
|
||||
EuiLoadingSpinner,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { ActionConnector } from '../../common';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
import {
|
||||
ACTION_TYPE_MODAL_EMPTY_TEXT,
|
||||
ACTION_TYPE_MODAL_EMPTY_TITLE,
|
||||
ACTION_TYPE_MODAL_FILTER_ALL,
|
||||
ACTION_TYPE_MODAL_TITLE,
|
||||
MODAL_SEARCH_CLEAR_FILTERS_TEXT,
|
||||
MODAL_SEARCH_PLACEHOLDER,
|
||||
} from '../translations';
|
||||
import { checkActionFormActionTypeEnabled } from '../utils/check_action_type_enabled';
|
||||
|
||||
type ConnectorsMap = Record<string, { actionTypeId: string; name: string; total: number }>;
|
||||
|
||||
export interface RuleActionsConnectorsModalProps {
|
||||
onClose: () => void;
|
||||
onSelectConnector: (connector: ActionConnector) => void;
|
||||
}
|
||||
|
||||
export const RuleActionsConnectorsModal = (props: RuleActionsConnectorsModalProps) => {
|
||||
const { onClose, onSelectConnector } = props;
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [selectedConnectorType, setSelectedConnectorType] = useState<string>('all');
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const currentBreakpoint = useCurrentEuiBreakpoint() ?? 'm';
|
||||
const isFullscreenPortrait = ['s', 'xs'].includes(currentBreakpoint);
|
||||
|
||||
const {
|
||||
plugins: { actionTypeRegistry },
|
||||
formData: { actions },
|
||||
connectors,
|
||||
connectorTypes,
|
||||
} = useRuleFormState();
|
||||
|
||||
const preconfiguredConnectors = useMemo(() => {
|
||||
return connectors.filter((connector) => connector.isPreconfigured);
|
||||
}, [connectors]);
|
||||
|
||||
const availableConnectors = useMemo(() => {
|
||||
return connectors.filter(({ actionTypeId }) => {
|
||||
const actionType = connectorTypes.find(({ id }) => id === actionTypeId);
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
|
||||
if (!actionType) {
|
||||
return false;
|
||||
}
|
||||
if (actionTypeModel.hideInUi) {
|
||||
return false;
|
||||
}
|
||||
if (!actionTypeModel.actionParamsFields) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionType,
|
||||
preconfiguredConnectors
|
||||
);
|
||||
|
||||
if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [connectors, connectorTypes, preconfiguredConnectors, actionTypeRegistry]);
|
||||
|
||||
const onSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onConnectorOptionSelect = useCallback(
|
||||
(id: string) => () => {
|
||||
setSelectedConnectorType((prev) => {
|
||||
if (prev === id) {
|
||||
return '';
|
||||
}
|
||||
return id;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onClearFilters = useCallback(() => {
|
||||
setSearchValue('');
|
||||
setSelectedConnectorType('all');
|
||||
}, []);
|
||||
|
||||
const connectorsMap: ConnectorsMap | null = useMemo(() => {
|
||||
return availableConnectors.reduce<ConnectorsMap>((result, { actionTypeId }) => {
|
||||
if (result[actionTypeId]) {
|
||||
result[actionTypeId].total += 1;
|
||||
} else {
|
||||
result[actionTypeId] = {
|
||||
actionTypeId,
|
||||
total: 1,
|
||||
name: connectorTypes.find(({ id }) => actionTypeId === id)?.name || '',
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
}, [availableConnectors, connectorTypes]);
|
||||
|
||||
const filteredConnectors = useMemo(() => {
|
||||
return availableConnectors
|
||||
.filter(({ actionTypeId }) => {
|
||||
if (selectedConnectorType === 'all' || selectedConnectorType === '') {
|
||||
return true;
|
||||
}
|
||||
if (selectedConnectorType === actionTypeId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.filter(({ actionTypeId, name }) => {
|
||||
const trimmedSearchValue = searchValue.trim().toLocaleLowerCase();
|
||||
if (trimmedSearchValue === '') {
|
||||
return true;
|
||||
}
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
const actionType = connectorTypes.find(({ id }) => id === actionTypeId);
|
||||
const textSearchTargets = [
|
||||
name.toLocaleLowerCase(),
|
||||
actionTypeModel.selectMessage?.toLocaleLowerCase(),
|
||||
actionTypeModel.actionTypeTitle?.toLocaleLowerCase(),
|
||||
actionType?.name?.toLocaleLowerCase(),
|
||||
];
|
||||
return textSearchTargets.some((text) => text?.includes(trimmedSearchValue));
|
||||
});
|
||||
}, [availableConnectors, selectedConnectorType, searchValue, connectorTypes, actionTypeRegistry]);
|
||||
|
||||
const connectorFacetButtons = useMemo(() => {
|
||||
return (
|
||||
<EuiFacetGroup data-test-subj="ruleActionsConnectorsModalFilterButtonGroup">
|
||||
<EuiFacetButton
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButton"
|
||||
key="all"
|
||||
quantity={availableConnectors.length}
|
||||
isSelected={selectedConnectorType === 'all'}
|
||||
onClick={onConnectorOptionSelect('all')}
|
||||
>
|
||||
{ACTION_TYPE_MODAL_FILTER_ALL}
|
||||
</EuiFacetButton>
|
||||
{Object.values(connectorsMap)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(({ actionTypeId, name, total }) => {
|
||||
return (
|
||||
<EuiFacetButton
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButton"
|
||||
key={actionTypeId}
|
||||
quantity={total}
|
||||
isSelected={selectedConnectorType === actionTypeId}
|
||||
onClick={onConnectorOptionSelect(actionTypeId)}
|
||||
>
|
||||
{name}
|
||||
</EuiFacetButton>
|
||||
);
|
||||
})}
|
||||
</EuiFacetGroup>
|
||||
);
|
||||
}, [availableConnectors, connectorsMap, selectedConnectorType, onConnectorOptionSelect]);
|
||||
|
||||
const connectorCards = useMemo(() => {
|
||||
if (!filteredConnectors.length) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="ruleActionsConnectorsModalEmpty"
|
||||
color="subdued"
|
||||
iconType="search"
|
||||
title={<h2>{ACTION_TYPE_MODAL_EMPTY_TITLE}</h2>}
|
||||
body={
|
||||
<EuiText>
|
||||
<p>{ACTION_TYPE_MODAL_EMPTY_TEXT}</p>
|
||||
</EuiText>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="ruleActionsConnectorsModalClearFiltersButton"
|
||||
size="s"
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
{MODAL_SEARCH_CLEAR_FILTERS_TEXT}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{filteredConnectors.map((connector) => {
|
||||
const { id, actionTypeId, name } = connector;
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
const actionType = connectorTypes.find((item) => item.id === actionTypeId);
|
||||
|
||||
if (!actionType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionType,
|
||||
preconfiguredConnectors
|
||||
);
|
||||
|
||||
const isSystemActionsSelected = Boolean(
|
||||
actionTypeModel.isSystemActionType &&
|
||||
actions.find((action) => action.actionTypeId === actionTypeModel.id)
|
||||
);
|
||||
|
||||
const isDisabled = !checkEnabledResult.isEnabled || isSystemActionsSelected;
|
||||
|
||||
const connectorCard = (
|
||||
<EuiCard
|
||||
data-test-subj="ruleActionsConnectorsModalCard"
|
||||
hasBorder
|
||||
isDisabled={isDisabled}
|
||||
titleSize="xs"
|
||||
layout="horizontal"
|
||||
icon={
|
||||
<div style={{ marginInlineEnd: `16px` }}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<EuiIcon size="l" type={actionTypeModel.iconClass} />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
title={name}
|
||||
description={
|
||||
<>
|
||||
<EuiText size="xs">{actionTypeModel.selectMessage}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued" size="xs" style={{ textTransform: 'uppercase' }}>
|
||||
<strong>{actionType?.name}</strong>
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
onClick={() => onSelectConnector(connector)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem key={id} grow={false}>
|
||||
{checkEnabledResult.isEnabled && connectorCard}
|
||||
{!checkEnabledResult.isEnabled && (
|
||||
<EuiToolTip position="top" content={checkEnabledResult.message}>
|
||||
{connectorCard}
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [
|
||||
actions,
|
||||
preconfiguredConnectors,
|
||||
filteredConnectors,
|
||||
actionTypeRegistry,
|
||||
connectorTypes,
|
||||
onSelectConnector,
|
||||
onClearFilters,
|
||||
]);
|
||||
|
||||
const responseiveHeight = isFullscreenPortrait ? 'initial' : '80vh';
|
||||
const responsiveOverflow = isFullscreenPortrait ? 'auto' : 'hidden';
|
||||
|
||||
return (
|
||||
<EuiModal
|
||||
onClose={onClose}
|
||||
maxWidth={euiTheme.breakpoint[currentBreakpoint]}
|
||||
style={{
|
||||
width: euiTheme.breakpoint[currentBreakpoint],
|
||||
maxHeight: responseiveHeight,
|
||||
height: responseiveHeight,
|
||||
overflow: responsiveOverflow,
|
||||
}}
|
||||
data-test-subj="ruleActionsConnectorsModal"
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle size="s">{ACTION_TYPE_MODAL_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiFlexGroup direction="column" style={{ overflow: responsiveOverflow, height: '100%' }}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="ruleActionsConnectorsModalSearch"
|
||||
placeholder={MODAL_SEARCH_PLACEHOLDER}
|
||||
value={searchValue}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ overflow: responsiveOverflow }}>
|
||||
<EuiFlexGroup style={{ overflow: responsiveOverflow }}>
|
||||
<EuiFlexItem grow={1}>{connectorFacetButtons}</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={3}
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
width: '100%',
|
||||
padding: `${euiTheme.size.base} ${euiTheme.size.base} ${euiTheme.size.xl}`,
|
||||
}}
|
||||
>
|
||||
{connectorCards}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RuleType } from '@kbn/alerting-types';
|
||||
import { ActionTypeModel, RuleTypeModel } from '../../common';
|
||||
import { TypeRegistry } from '../../common/type_registry';
|
||||
import {
|
||||
getAction,
|
||||
getActionType,
|
||||
getActionTypeModel,
|
||||
getConnector,
|
||||
} from '../../common/test_utils/actions_test_utils';
|
||||
import { RuleActionsItem } from './rule_actions_item';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { RuleActionsSettingsProps } from './rule_actions_settings';
|
||||
import { RuleActionsMessageProps } from './rule_actions_message';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_settings', () => ({
|
||||
RuleActionsSettings: ({
|
||||
onNotifyWhenChange,
|
||||
onActionGroupChange,
|
||||
onAlertsFilterChange,
|
||||
onTimeframeChange,
|
||||
}: RuleActionsSettingsProps) => (
|
||||
<div>
|
||||
ruleActionsSettings
|
||||
<button
|
||||
onClick={() =>
|
||||
onNotifyWhenChange({
|
||||
summary: true,
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
throttle: '5m',
|
||||
})
|
||||
}
|
||||
>
|
||||
onNotifyWhenChange
|
||||
</button>
|
||||
<button onClick={() => onActionGroupChange('recovered')}>onActionGroupChange</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onAlertsFilterChange({
|
||||
kql: '',
|
||||
filters: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
onAlertsFilterChange
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onTimeframeChange({
|
||||
days: [1, 2, 3],
|
||||
timezone: 'UTC',
|
||||
hours: {
|
||||
start: 'now',
|
||||
end: 'now',
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
onTimeframeChange
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_message', () => ({
|
||||
RuleActionsMessage: ({ onParamsChange, templateFields }: RuleActionsMessageProps) => (
|
||||
<div>
|
||||
ruleActionsMessage
|
||||
<button onClick={() => onParamsChange('paramsKey', { paramsKey: 'paramsValue' })}>
|
||||
onParamsChange
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../validation/validate_params_for_warnings', () => ({
|
||||
validateParamsForWarnings: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../action_variables/get_available_action_variables', () => ({
|
||||
getAvailableActionVariables: jest.fn(),
|
||||
}));
|
||||
|
||||
const ruleType = {
|
||||
id: '.es-query',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'testActionGroup',
|
||||
name: 'Test Action Group',
|
||||
},
|
||||
{
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'testActionGroup',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: {
|
||||
id: 'recovered',
|
||||
},
|
||||
producer: 'logs',
|
||||
authorizedConsumers: {
|
||||
alerting: { read: true, all: true },
|
||||
test: { read: true, all: true },
|
||||
stackAlerts: { read: true, all: true },
|
||||
logs: { read: true, all: true },
|
||||
},
|
||||
actionVariables: {
|
||||
params: [],
|
||||
state: [],
|
||||
},
|
||||
enabledInLicense: true,
|
||||
} as unknown as RuleType;
|
||||
|
||||
const ruleModel: RuleTypeModel = {
|
||||
id: '.es-query',
|
||||
description: 'Sample rule type model',
|
||||
iconClass: 'sampleIconClass',
|
||||
documentationUrl: 'testurl',
|
||||
validate: (params, isServerless) => ({ errors: {} }),
|
||||
ruleParamsExpression: () => <div>Expression</div>,
|
||||
defaultSummaryMessage: 'Sample default summary message',
|
||||
defaultActionMessage: 'Sample default action message',
|
||||
defaultRecoveryMessage: 'Sample default recovery message',
|
||||
requiresAppContext: false,
|
||||
};
|
||||
|
||||
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
|
||||
|
||||
const { validateParamsForWarnings } = jest.requireMock(
|
||||
'../validation/validate_params_for_warnings'
|
||||
);
|
||||
|
||||
const { getAvailableActionVariables } = jest.requireMock(
|
||||
'../../action_variables/get_available_action_variables'
|
||||
);
|
||||
|
||||
const mockConnectors = [getConnector('1', { id: 'action-1' })];
|
||||
|
||||
const mockActionTypes = [getActionType('1')];
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
const mockValidate = jest.fn().mockResolvedValue({
|
||||
errors: {},
|
||||
});
|
||||
|
||||
describe('ruleActionsItem', () => {
|
||||
beforeEach(() => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('1', {
|
||||
id: 'actionType-1',
|
||||
defaultRecoveredActionParams: { recoveredParamKey: 'recoveredParamValue' },
|
||||
defaultActionParams: { actionParamKey: 'actionParamValue' },
|
||||
validateParams: mockValidate,
|
||||
})
|
||||
);
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
http: {
|
||||
basePath: {
|
||||
publicBaseUrl: 'publicUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockActionTypes,
|
||||
aadTemplateFields: [],
|
||||
actionsParamsErrors: {},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
});
|
||||
useRuleFormDispatch.mockReturnValue(mockOnChange);
|
||||
validateParamsForWarnings.mockReturnValue(null);
|
||||
getAvailableActionVariables.mockReturnValue(['mockActionVariable']);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render correctly', () => {
|
||||
render(
|
||||
<RuleActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ruleActionsItem')).toBeInTheDocument();
|
||||
expect(screen.queryByText('ruleActionsSettings')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('ruleActionsMessage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow for toggling between setting and message', async () => {
|
||||
render(
|
||||
<RuleActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Message'));
|
||||
|
||||
expect(screen.getByText('ruleActionsMessage')).toBeInTheDocument();
|
||||
expect(screen.queryByText('ruleActionsSettings')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('Settings'));
|
||||
|
||||
expect(screen.getByText('ruleActionsSettings')).toBeInTheDocument();
|
||||
expect(screen.queryByText('ruleActionsMessage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow notify when to be changed', async () => {
|
||||
render(
|
||||
<RuleActionsItem
|
||||
action={getAction('1', {
|
||||
actionTypeId: 'actionType-1',
|
||||
group: 'recovered',
|
||||
})}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Settings'));
|
||||
|
||||
await userEvent.click(screen.getByText('onNotifyWhenChange'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { uuid: 'uuid-action-1', value: { recoveredParamKey: 'recoveredParamValue' } },
|
||||
type: 'setActionParams',
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
key: 'frequency',
|
||||
uuid: 'uuid-action-1',
|
||||
value: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '5m' },
|
||||
},
|
||||
type: 'setActionProperty',
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { errors: {}, uuid: 'uuid-action-1' },
|
||||
type: 'setActionParamsError',
|
||||
});
|
||||
|
||||
expect(getAvailableActionVariables).toHaveBeenCalledWith(
|
||||
{ params: [], state: [] },
|
||||
undefined,
|
||||
{
|
||||
defaultActionMessage: 'Sample default recovery message',
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
omitMessageVariables: 'all',
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('should allow alerts filter to be changed', async () => {
|
||||
render(
|
||||
<RuleActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Settings'));
|
||||
|
||||
await userEvent.click(screen.getByText('onAlertsFilterChange'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
key: 'alertsFilter',
|
||||
uuid: 'uuid-action-1',
|
||||
value: { query: { filters: [], kql: '' } },
|
||||
},
|
||||
type: 'setActionProperty',
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { errors: { filterQuery: ['A custom query is required.'] }, uuid: 'uuid-action-1' },
|
||||
type: 'setActionError',
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow timeframe to be changed', async () => {
|
||||
render(
|
||||
<RuleActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Settings'));
|
||||
|
||||
await userEvent.click(screen.getByText('onTimeframeChange'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
key: 'alertsFilter',
|
||||
uuid: 'uuid-action-1',
|
||||
value: {
|
||||
timeframe: { days: [1, 2, 3], hours: { end: 'now', start: 'now' }, timezone: 'UTC' },
|
||||
},
|
||||
},
|
||||
type: 'setActionProperty',
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow params to be changed', async () => {
|
||||
render(
|
||||
<RuleActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Message'));
|
||||
|
||||
await userEvent.click(screen.getByText('onParamsChange'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { uuid: 'uuid-action-1', value: { paramsKey: { paramsKey: 'paramsValue' } } },
|
||||
type: 'setActionParams',
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { errors: {}, uuid: 'uuid-action-1' },
|
||||
type: 'setActionParamsError',
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow action to be deleted', async () => {
|
||||
render(
|
||||
<RuleActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Settings'));
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsItemDeleteButton'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { uuid: 'uuid-action-1' },
|
||||
type: 'removeAction',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,686 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiAccordion,
|
||||
EuiPanel,
|
||||
EuiButtonIcon,
|
||||
useEuiTheme,
|
||||
useEuiBackgroundColor,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiTabs,
|
||||
EuiTab,
|
||||
EuiToolTip,
|
||||
EuiBadge,
|
||||
RecursivePartial,
|
||||
EuiBetaBadge,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
ActionVariable,
|
||||
AlertsFilter,
|
||||
AlertsFilterTimeframe,
|
||||
RuleAction,
|
||||
RuleActionFrequency,
|
||||
RuleActionParam,
|
||||
RuleActionParams,
|
||||
} from '@kbn/alerting-types';
|
||||
import { isEmpty, some } from 'lodash';
|
||||
import { css } from '@emotion/react';
|
||||
import { SavedObjectAttribute } from '@kbn/core/types';
|
||||
import { useRuleFormDispatch, useRuleFormState } from '../hooks';
|
||||
import {
|
||||
ActionConnector,
|
||||
ActionTypeModel,
|
||||
RuleFormParamsErrors,
|
||||
RuleTypeWithDescription,
|
||||
} from '../../common/types';
|
||||
import { getAvailableActionVariables } from '../../action_variables';
|
||||
import { validateAction, validateParamsForWarnings } from '../validation';
|
||||
|
||||
import { RuleActionsSettings } from './rule_actions_settings';
|
||||
import { getSelectedActionGroup } from '../utils';
|
||||
import { RuleActionsMessage } from './rule_actions_message';
|
||||
import {
|
||||
ACTION_ERROR_TOOLTIP,
|
||||
ACTION_UNABLE_TO_LOAD_CONNECTOR_DESCRIPTION,
|
||||
ACTION_UNABLE_TO_LOAD_CONNECTOR_TITLE,
|
||||
ACTION_WARNING_TITLE,
|
||||
TECH_PREVIEW_DESCRIPTION,
|
||||
TECH_PREVIEW_LABEL,
|
||||
} from '../translations';
|
||||
|
||||
const SUMMARY_GROUP_TITLE = i18n.translate('alertsUIShared.ruleActionsItem.summaryGroupTitle', {
|
||||
defaultMessage: 'Summary of alerts',
|
||||
});
|
||||
|
||||
const RUN_WHEN_GROUP_TITLE = (groupName: string) =>
|
||||
i18n.translate('alertsUIShared.ruleActionsItem.runWhenGroupTitle', {
|
||||
defaultMessage: 'Run when {groupName}',
|
||||
values: {
|
||||
groupName,
|
||||
},
|
||||
});
|
||||
|
||||
const ACTION_TITLE = (connector: ActionConnector) =>
|
||||
i18n.translate('alertsUIShared.ruleActionsItem.existingAlertActionTypeEditTitle', {
|
||||
defaultMessage: '{actionConnectorName}',
|
||||
values: {
|
||||
actionConnectorName: `${connector.name} ${
|
||||
connector.isPreconfigured ? '(preconfigured)' : ''
|
||||
}`,
|
||||
},
|
||||
});
|
||||
|
||||
const getDefaultParams = ({
|
||||
group,
|
||||
ruleType,
|
||||
actionTypeModel,
|
||||
}: {
|
||||
group: string;
|
||||
actionTypeModel: ActionTypeModel;
|
||||
ruleType: RuleTypeWithDescription;
|
||||
}) => {
|
||||
if (group === ruleType.recoveryActionGroup.id) {
|
||||
return actionTypeModel.defaultRecoveredActionParams;
|
||||
} else {
|
||||
return actionTypeModel.defaultActionParams;
|
||||
}
|
||||
};
|
||||
|
||||
export interface RuleActionsItemProps {
|
||||
action: RuleAction;
|
||||
index: number;
|
||||
producerId: string;
|
||||
}
|
||||
|
||||
type ParamsType = RecursivePartial<any>;
|
||||
|
||||
const MESSAGES_TAB = 'messages';
|
||||
const SETTINGS_TAB = 'settings';
|
||||
|
||||
export const RuleActionsItem = (props: RuleActionsItemProps) => {
|
||||
const { action, index, producerId } = props;
|
||||
|
||||
const {
|
||||
plugins: { actionTypeRegistry, http },
|
||||
actionsParamsErrors = {},
|
||||
selectedRuleType,
|
||||
selectedRuleTypeModel,
|
||||
connectors,
|
||||
connectorTypes,
|
||||
aadTemplateFields,
|
||||
} = useRuleFormState();
|
||||
|
||||
const [tab, setTab] = useState<string>(MESSAGES_TAB);
|
||||
const subdued = useEuiBackgroundColor('subdued');
|
||||
const plain = useEuiBackgroundColor('plain');
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const [availableActionVariables, setAvailableActionVariables] = useState<ActionVariable[]>(() => {
|
||||
if (!selectedRuleType.actionVariables) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedActionGroup = getSelectedActionGroup({
|
||||
group: action.group,
|
||||
ruleType: selectedRuleType,
|
||||
ruleTypeModel: selectedRuleTypeModel,
|
||||
});
|
||||
|
||||
return getAvailableActionVariables(
|
||||
selectedRuleType.actionVariables,
|
||||
// TODO: this is always undefined for now, might need to make this a prop later on
|
||||
undefined,
|
||||
selectedActionGroup,
|
||||
!!action.frequency?.summary
|
||||
);
|
||||
});
|
||||
|
||||
const [useDefaultMessage, setUseDefaultMessage] = useState(false);
|
||||
|
||||
const [storedActionParamsForAadToggle, setStoredActionParamsForAadToggle] = useState<
|
||||
Record<string, SavedObjectAttribute>
|
||||
>({});
|
||||
|
||||
const [warning, setWarning] = useState<string | null>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const dispatch = useRuleFormDispatch();
|
||||
const actionTypeModel = actionTypeRegistry.get(action.actionTypeId);
|
||||
const actionType = connectorTypes.find(({ id }) => id === action.actionTypeId);
|
||||
const connector = connectors.find(({ id }) => id === action.id);
|
||||
|
||||
const showActionGroupErrorIcon = useMemo(() => {
|
||||
const actionParamsError = actionsParamsErrors[action.uuid!] || {};
|
||||
return !isOpen && some(actionParamsError, (error) => !isEmpty(error));
|
||||
}, [isOpen, action, actionsParamsErrors]);
|
||||
|
||||
const selectedActionGroup = getSelectedActionGroup({
|
||||
group: action.group,
|
||||
ruleType: selectedRuleType,
|
||||
ruleTypeModel: selectedRuleTypeModel,
|
||||
});
|
||||
|
||||
const templateFields = action.useAlertDataForTemplate
|
||||
? aadTemplateFields
|
||||
: availableActionVariables;
|
||||
|
||||
const onDelete = (id: string) => {
|
||||
dispatch({ type: 'removeAction', payload: { uuid: id } });
|
||||
};
|
||||
|
||||
const validateActionBase = useCallback(
|
||||
(newAction: RuleAction) => {
|
||||
const errors = validateAction({ action: newAction });
|
||||
dispatch({
|
||||
type: 'setActionError',
|
||||
payload: {
|
||||
uuid: newAction.uuid!,
|
||||
errors,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const validateActionParams = useCallback(
|
||||
async (params: RuleActionParam) => {
|
||||
const res: { errors: RuleFormParamsErrors } = await actionTypeRegistry
|
||||
.get(action.actionTypeId)
|
||||
?.validateParams(params);
|
||||
|
||||
dispatch({
|
||||
type: 'setActionParamsError',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
errors: res.errors,
|
||||
},
|
||||
});
|
||||
},
|
||||
[actionTypeRegistry, action, dispatch]
|
||||
);
|
||||
|
||||
const onStoredActionParamsChange = useCallback(
|
||||
(
|
||||
aadParams: Record<string, SavedObjectAttribute>,
|
||||
params: Record<string, SavedObjectAttribute>
|
||||
) => {
|
||||
if (isEmpty(aadParams) && action.params.subAction) {
|
||||
setStoredActionParamsForAadToggle(params);
|
||||
} else {
|
||||
setStoredActionParamsForAadToggle(aadParams);
|
||||
}
|
||||
},
|
||||
[action]
|
||||
);
|
||||
|
||||
const onAvailableActionVariablesChange = useCallback(
|
||||
({ actionGroup, summary: isSummaryAction }: { actionGroup: string; summary?: boolean }) => {
|
||||
const messageVariables = selectedRuleType.actionVariables;
|
||||
|
||||
if (!messageVariables) {
|
||||
setAvailableActionVariables([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelectedActionGroup = getSelectedActionGroup({
|
||||
group: actionGroup,
|
||||
ruleType: selectedRuleType,
|
||||
ruleTypeModel: selectedRuleTypeModel,
|
||||
});
|
||||
|
||||
setAvailableActionVariables(
|
||||
getAvailableActionVariables(
|
||||
messageVariables,
|
||||
undefined,
|
||||
newSelectedActionGroup,
|
||||
!!isSummaryAction
|
||||
)
|
||||
);
|
||||
},
|
||||
[selectedRuleType, selectedRuleTypeModel]
|
||||
);
|
||||
|
||||
const setDefaultParams = useCallback(
|
||||
(actionGroup: string) => {
|
||||
const defaultParams = getDefaultParams({
|
||||
group: actionGroup,
|
||||
ruleType: selectedRuleType,
|
||||
actionTypeModel,
|
||||
});
|
||||
|
||||
if (!defaultParams) {
|
||||
return;
|
||||
}
|
||||
const newDefaultParams: ParamsType = {};
|
||||
const defaultAADParams: ParamsType = {};
|
||||
for (const [key, paramValue] of Object.entries(defaultParams)) {
|
||||
newDefaultParams[key] = paramValue;
|
||||
// Collects AAD params by checking if the value is {x}.{y}
|
||||
if (typeof paramValue !== 'string' || !paramValue.match(/{{.*?}}/g)) {
|
||||
defaultAADParams[key] = paramValue;
|
||||
}
|
||||
}
|
||||
const newParams = {
|
||||
...action.params,
|
||||
...newDefaultParams,
|
||||
};
|
||||
dispatch({
|
||||
type: 'setActionParams',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
value: newParams,
|
||||
},
|
||||
});
|
||||
validateActionParams(newParams);
|
||||
onStoredActionParamsChange(defaultAADParams, newParams);
|
||||
},
|
||||
[
|
||||
action,
|
||||
dispatch,
|
||||
validateActionParams,
|
||||
selectedRuleType,
|
||||
actionTypeModel,
|
||||
onStoredActionParamsChange,
|
||||
]
|
||||
);
|
||||
|
||||
const onDefaultParamsChange = useCallback(
|
||||
(actionGroup: string, summary?: boolean) => {
|
||||
onAvailableActionVariablesChange({
|
||||
actionGroup,
|
||||
summary,
|
||||
});
|
||||
setDefaultParams(actionGroup);
|
||||
},
|
||||
[onAvailableActionVariablesChange, setDefaultParams]
|
||||
);
|
||||
|
||||
const onParamsChange = useCallback(
|
||||
(key: string, value: RuleActionParam) => {
|
||||
const newParams = {
|
||||
...action.params,
|
||||
[key]: value,
|
||||
};
|
||||
dispatch({
|
||||
type: 'setActionParams',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
value: newParams,
|
||||
},
|
||||
});
|
||||
setWarning(
|
||||
validateParamsForWarnings({
|
||||
value,
|
||||
publicBaseUrl: http.basePath.publicBaseUrl,
|
||||
actionVariables: availableActionVariables,
|
||||
})
|
||||
);
|
||||
validateActionParams(newParams);
|
||||
onStoredActionParamsChange(storedActionParamsForAadToggle, newParams);
|
||||
},
|
||||
[
|
||||
http,
|
||||
action,
|
||||
availableActionVariables,
|
||||
dispatch,
|
||||
validateActionParams,
|
||||
onStoredActionParamsChange,
|
||||
storedActionParamsForAadToggle,
|
||||
]
|
||||
);
|
||||
|
||||
const onNotifyWhenChange = useCallback(
|
||||
(frequency: RuleActionFrequency) => {
|
||||
dispatch({
|
||||
type: 'setActionProperty',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
key: 'frequency',
|
||||
value: frequency,
|
||||
},
|
||||
});
|
||||
if (frequency.summary !== action.frequency?.summary) {
|
||||
onDefaultParamsChange(action.group, frequency.summary);
|
||||
}
|
||||
},
|
||||
[action, onDefaultParamsChange, dispatch]
|
||||
);
|
||||
|
||||
const onActionGroupChange = useCallback(
|
||||
(group: string) => {
|
||||
dispatch({
|
||||
type: 'setActionProperty',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
key: 'group',
|
||||
value: group,
|
||||
},
|
||||
});
|
||||
onDefaultParamsChange(group, action.frequency?.summary);
|
||||
},
|
||||
[action, onDefaultParamsChange, dispatch]
|
||||
);
|
||||
|
||||
const onAlertsFilterChange = useCallback(
|
||||
(query?: AlertsFilter['query']) => {
|
||||
const newAlertsFilter = {
|
||||
...action.alertsFilter,
|
||||
query,
|
||||
};
|
||||
const newAction = {
|
||||
...action,
|
||||
alertsFilter: newAlertsFilter,
|
||||
};
|
||||
dispatch({
|
||||
type: 'setActionProperty',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
key: 'alertsFilter',
|
||||
value: newAlertsFilter,
|
||||
},
|
||||
});
|
||||
validateActionBase(newAction);
|
||||
},
|
||||
[action, dispatch, validateActionBase]
|
||||
);
|
||||
|
||||
const onTimeframeChange = useCallback(
|
||||
(timeframe?: AlertsFilterTimeframe) => {
|
||||
dispatch({
|
||||
type: 'setActionProperty',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
key: 'alertsFilter',
|
||||
value: {
|
||||
...action.alertsFilter,
|
||||
timeframe,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[action, dispatch]
|
||||
);
|
||||
|
||||
const onUseAadTemplateFieldsChange = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'setActionProperty',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
key: 'useAlertDataForTemplate',
|
||||
value: !!!action.useAlertDataForTemplate,
|
||||
},
|
||||
});
|
||||
|
||||
const currentActionParams = { ...action.params };
|
||||
const newActionParams: RuleActionParams = {};
|
||||
for (const key of Object.keys(currentActionParams)) {
|
||||
newActionParams[key] = storedActionParamsForAadToggle[key] ?? '';
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'setActionParams',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
value: newActionParams,
|
||||
},
|
||||
});
|
||||
|
||||
setStoredActionParamsForAadToggle(currentActionParams);
|
||||
}, [action, storedActionParamsForAadToggle, dispatch]);
|
||||
|
||||
const accordionContent = useMemo(() => {
|
||||
if (!connector) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
style={{
|
||||
padding: euiTheme.size.l,
|
||||
backgroundColor: plain,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
}}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTabs>
|
||||
<EuiTab isSelected={tab === MESSAGES_TAB} onClick={() => setTab(MESSAGES_TAB)}>
|
||||
Message
|
||||
</EuiTab>
|
||||
<EuiTab isSelected={tab === SETTINGS_TAB} onClick={() => setTab(SETTINGS_TAB)}>
|
||||
Settings
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{tab === MESSAGES_TAB && (
|
||||
<RuleActionsMessage
|
||||
action={action}
|
||||
index={index}
|
||||
useDefaultMessage={useDefaultMessage}
|
||||
connector={connector}
|
||||
producerId={producerId}
|
||||
warning={warning}
|
||||
templateFields={templateFields}
|
||||
onParamsChange={onParamsChange}
|
||||
onUseAadTemplateFieldsChange={onUseAadTemplateFieldsChange}
|
||||
/>
|
||||
)}
|
||||
{tab === SETTINGS_TAB && (
|
||||
<RuleActionsSettings
|
||||
action={action}
|
||||
producerId={producerId}
|
||||
onUseDefaultMessageChange={() => setUseDefaultMessage(true)}
|
||||
onNotifyWhenChange={onNotifyWhenChange}
|
||||
onActionGroupChange={onActionGroupChange}
|
||||
onAlertsFilterChange={onAlertsFilterChange}
|
||||
onTimeframeChange={onTimeframeChange}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [
|
||||
action,
|
||||
connector,
|
||||
producerId,
|
||||
euiTheme,
|
||||
plain,
|
||||
index,
|
||||
tab,
|
||||
templateFields,
|
||||
useDefaultMessage,
|
||||
warning,
|
||||
onNotifyWhenChange,
|
||||
onActionGroupChange,
|
||||
onAlertsFilterChange,
|
||||
onTimeframeChange,
|
||||
onParamsChange,
|
||||
onUseAadTemplateFieldsChange,
|
||||
]);
|
||||
|
||||
const noConnectorContent = useMemo(() => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="magnifyWithExclamation"
|
||||
title={<h2>{ACTION_UNABLE_TO_LOAD_CONNECTOR_TITLE}</h2>}
|
||||
body={ACTION_UNABLE_TO_LOAD_CONNECTOR_DESCRIPTION}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const accordionIcon = useMemo(() => {
|
||||
if (!connector) {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={ACTION_UNABLE_TO_LOAD_CONNECTOR_TITLE}>
|
||||
<EuiIcon
|
||||
data-test-subj="action-group-error-icon"
|
||||
type="warning"
|
||||
color="danger"
|
||||
size="l"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
{showActionGroupErrorIcon ? (
|
||||
<EuiToolTip content={ACTION_ERROR_TOOLTIP}>
|
||||
<EuiIcon
|
||||
data-test-subj="action-group-error-icon"
|
||||
type="warning"
|
||||
color="danger"
|
||||
size="l"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<Suspense fallback={null}>
|
||||
<EuiIcon size="l" type={actionTypeModel.iconClass} />
|
||||
</Suspense>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}, [connector, showActionGroupErrorIcon, actionTypeModel]);
|
||||
|
||||
const connectorTitle = useMemo(() => {
|
||||
const title = connector ? ACTION_TITLE(connector) : actionTypeModel.actionTypeTitle;
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>{title}</EuiText>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}, [connector, actionTypeModel]);
|
||||
|
||||
const actionTypeTitle = useMemo(() => {
|
||||
if (!connector || !actionType) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
<strong>{actionType.name}</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}, [connector, actionType]);
|
||||
|
||||
const runWhenTitle = useMemo(() => {
|
||||
if (!connector) {
|
||||
return null;
|
||||
}
|
||||
if (isOpen) {
|
||||
return null;
|
||||
}
|
||||
if (selectedActionGroup || action.frequency?.summary) {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge iconType="clock">
|
||||
{action.frequency?.summary
|
||||
? SUMMARY_GROUP_TITLE
|
||||
: RUN_WHEN_GROUP_TITLE(selectedActionGroup!.name.toLocaleLowerCase())}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
}, [connector, isOpen, selectedActionGroup, action]);
|
||||
|
||||
const warningIcon = useMemo(() => {
|
||||
if (!connector) {
|
||||
return null;
|
||||
}
|
||||
if (isOpen) {
|
||||
return null;
|
||||
}
|
||||
if (warning) {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge data-test-subj="warning-badge" iconType="warning" color="warning">
|
||||
{ACTION_WARNING_TITLE}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
}, [connector, isOpen, warning]);
|
||||
|
||||
return (
|
||||
<EuiAccordion
|
||||
initialIsOpen
|
||||
data-test-subj="ruleActionsItem"
|
||||
borders="all"
|
||||
style={{
|
||||
backgroundColor: subdued,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
}}
|
||||
id={action.id}
|
||||
onToggle={setIsOpen}
|
||||
buttonProps={{
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
arrowProps={{
|
||||
css: css`
|
||||
margin-left: ${euiTheme.size.m};
|
||||
`,
|
||||
}}
|
||||
extraAction={
|
||||
<EuiButtonIcon
|
||||
data-test-subj="ruleActionsItemDeleteButton"
|
||||
style={{
|
||||
marginRight: euiTheme.size.l,
|
||||
}}
|
||||
aria-label={i18n.translate(
|
||||
'alertsUIShared.ruleActionsSystemActionsItem.deleteActionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'delete action',
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={() => onDelete(action.uuid!)}
|
||||
/>
|
||||
}
|
||||
buttonContentClassName="eui-fullWidth"
|
||||
buttonContent={
|
||||
<EuiPanel color="subdued" paddingSize="m">
|
||||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
{accordionIcon}
|
||||
{connectorTitle}
|
||||
{actionTypeTitle}
|
||||
{runWhenTitle}
|
||||
{warningIcon}
|
||||
{actionTypeModel.isExperimental && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
alignment="middle"
|
||||
data-test-subj="ruleActionsSystemActionsItemBetaBadge"
|
||||
label={TECH_PREVIEW_LABEL}
|
||||
tooltipContent={TECH_PREVIEW_DESCRIPTION}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
}
|
||||
>
|
||||
{connector && accordionContent}
|
||||
{!connector && noConnectorContent}
|
||||
</EuiAccordion>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,353 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { lazy } from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { RuleActionsMessage } from './rule_actions_message';
|
||||
import { RuleType } from '@kbn/alerting-types';
|
||||
import { ActionParamsProps, ActionTypeModel, RuleTypeModel } from '../../common';
|
||||
import { TypeRegistry } from '../../common/type_registry';
|
||||
import {
|
||||
getAction,
|
||||
getActionType,
|
||||
getActionTypeModel,
|
||||
getConnector,
|
||||
getSystemAction,
|
||||
} from '../../common/test_utils/actions_test_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
}));
|
||||
|
||||
const { useRuleFormState } = jest.requireMock('../hooks');
|
||||
|
||||
const ruleType = {
|
||||
id: '.es-query',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'testActionGroup',
|
||||
name: 'Test Action Group',
|
||||
},
|
||||
{
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'testActionGroup',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: {
|
||||
id: 'recovered',
|
||||
},
|
||||
producer: 'logs',
|
||||
authorizedConsumers: {
|
||||
alerting: { read: true, all: true },
|
||||
test: { read: true, all: true },
|
||||
stackAlerts: { read: true, all: true },
|
||||
logs: { read: true, all: true },
|
||||
},
|
||||
actionVariables: {
|
||||
params: [],
|
||||
state: [],
|
||||
},
|
||||
enabledInLicense: true,
|
||||
} as unknown as RuleType;
|
||||
|
||||
const ruleModel: RuleTypeModel = {
|
||||
id: '.es-query',
|
||||
description: 'Sample rule type model',
|
||||
iconClass: 'sampleIconClass',
|
||||
documentationUrl: 'testurl',
|
||||
validate: (params, isServerless) => ({ errors: {} }),
|
||||
ruleParamsExpression: () => <div>Expression</div>,
|
||||
defaultSummaryMessage: 'Sample default summary message',
|
||||
defaultActionMessage: 'Sample default action message',
|
||||
defaultRecoveryMessage: 'Sample default recovery message',
|
||||
requiresAppContext: false,
|
||||
};
|
||||
|
||||
const mockOnParamsChange = jest.fn();
|
||||
|
||||
const mockedActionParamsFields = lazy(async () => ({
|
||||
default({ defaultMessage, selectedActionGroupId, errors, editAction }: ActionParamsProps<any>) {
|
||||
return (
|
||||
<div data-test-subj="actionParamsFieldMock">
|
||||
{defaultMessage && <div data-test-subj="defaultMessageMock">{defaultMessage}</div>}
|
||||
{selectedActionGroupId && (
|
||||
<div data-test-subj="selectedActionGroupIdMock">{selectedActionGroupId}</div>
|
||||
)}
|
||||
<div data-test-subj="errorsMock">{JSON.stringify(errors)}</div>
|
||||
<button
|
||||
data-test-subj="editActionMock"
|
||||
onClick={() => editAction('paramsKey', { paramsKey: 'paramsValue' }, 1)}
|
||||
>
|
||||
editAction
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
describe('RuleActionsMessage', () => {
|
||||
beforeEach(() => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('1', {
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
})
|
||||
);
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
actionsParamsErrors: {},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
connectors: [getConnector('1')],
|
||||
connectorTypes: [getActionType('1')],
|
||||
aadTemplateFields: [],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should render correctly', async () => {
|
||||
render(
|
||||
<RuleActionsMessage
|
||||
action={getAction('1', { actionTypeId: 'actionTypeModel-1' })}
|
||||
index={1}
|
||||
templateFields={[]}
|
||||
useDefaultMessage
|
||||
connector={getConnector('1')}
|
||||
producerId="stackAlerts"
|
||||
onParamsChange={mockOnParamsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(screen.getByTestId('actionParamsFieldMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('ruleActionsMessage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should display warning if it exists', async () => {
|
||||
render(
|
||||
<RuleActionsMessage
|
||||
action={getAction('1', { actionTypeId: 'actionTypeModel-1' })}
|
||||
index={1}
|
||||
templateFields={[]}
|
||||
useDefaultMessage
|
||||
connector={getConnector('1')}
|
||||
producerId="stackAlerts"
|
||||
warning="test warning"
|
||||
onParamsChange={mockOnParamsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(screen.getByTestId('actionParamsFieldMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('test warning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render default action message for normal actions', async () => {
|
||||
render(
|
||||
<RuleActionsMessage
|
||||
action={getAction('1', { actionTypeId: 'actionTypeModel-1' })}
|
||||
index={1}
|
||||
templateFields={[]}
|
||||
useDefaultMessage
|
||||
connector={getConnector('1')}
|
||||
producerId="stackAlerts"
|
||||
warning="test warning"
|
||||
onParamsChange={mockOnParamsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(screen.getByTestId('actionParamsFieldMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Sample default action message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render default summary message for actions with summaries', async () => {
|
||||
render(
|
||||
<RuleActionsMessage
|
||||
action={getAction('1', {
|
||||
actionTypeId: 'actionTypeModel-1',
|
||||
frequency: {
|
||||
summary: true,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
throttle: '5m',
|
||||
},
|
||||
})}
|
||||
index={1}
|
||||
templateFields={[]}
|
||||
useDefaultMessage
|
||||
connector={getConnector('1')}
|
||||
producerId="stackAlerts"
|
||||
warning="test warning"
|
||||
onParamsChange={mockOnParamsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(screen.getByTestId('actionParamsFieldMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Sample default summary message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render default recovery message for action recovery group', async () => {
|
||||
render(
|
||||
<RuleActionsMessage
|
||||
action={getAction('1', {
|
||||
actionTypeId: 'actionTypeModel-1',
|
||||
group: 'recovered',
|
||||
})}
|
||||
index={1}
|
||||
templateFields={[]}
|
||||
useDefaultMessage
|
||||
connector={getConnector('1')}
|
||||
producerId="stackAlerts"
|
||||
warning="test warning"
|
||||
onParamsChange={mockOnParamsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(screen.getByTestId('actionParamsFieldMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Sample default recovery message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render default summary message for system actions', async () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('1', {
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
})
|
||||
);
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
actionsParamsErrors: {},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
connectors: [getConnector('1')],
|
||||
connectorTypes: [
|
||||
getActionType('1', {
|
||||
isSystemActionType: true,
|
||||
id: 'actionTypeModel-1',
|
||||
}),
|
||||
],
|
||||
aadTemplateFields: [],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsMessage
|
||||
action={getSystemAction('1', {
|
||||
actionTypeId: 'actionTypeModel-1',
|
||||
})}
|
||||
index={1}
|
||||
templateFields={[]}
|
||||
useDefaultMessage
|
||||
connector={getConnector('1')}
|
||||
producerId="stackAlerts"
|
||||
warning="test warning"
|
||||
onParamsChange={mockOnParamsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(screen.getByTestId('actionParamsFieldMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Sample default summary message')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('selectedActionGroupIdMock')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render action param errors if it exists', async () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('1', {
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
})
|
||||
);
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
actionsParamsErrors: {
|
||||
'uuid-action-1': { paramsKey: 'error' },
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
connectors: [getConnector('1')],
|
||||
connectorTypes: [getActionType('1')],
|
||||
aadTemplateFields: [],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsMessage
|
||||
action={getAction('1', { actionTypeId: 'actionTypeModel-1' })}
|
||||
index={1}
|
||||
templateFields={[]}
|
||||
useDefaultMessage
|
||||
connector={getConnector('1')}
|
||||
producerId="stackAlerts"
|
||||
warning="test warning"
|
||||
onParamsChange={mockOnParamsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(screen.getByTestId('actionParamsFieldMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText(JSON.stringify({ paramsKey: 'error' }))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onParamsChange if the params are edited', async () => {
|
||||
render(
|
||||
<RuleActionsMessage
|
||||
action={getAction('1', { actionTypeId: 'actionTypeModel-1' })}
|
||||
index={1}
|
||||
templateFields={[]}
|
||||
useDefaultMessage
|
||||
connector={getConnector('1')}
|
||||
producerId="stackAlerts"
|
||||
warning="test warning"
|
||||
onParamsChange={mockOnParamsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(screen.getByTestId('actionParamsFieldMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByTestId('editActionMock'));
|
||||
expect(mockOnParamsChange).toHaveBeenLastCalledWith(
|
||||
'paramsKey',
|
||||
{ paramsKey: 'paramsValue' },
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { Suspense, useMemo } from 'react';
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiErrorBoundary,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
} from '@elastic/eui';
|
||||
import { ActionVariable, RuleActionParam } from '@kbn/alerting-types';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
import { ActionConnector, ActionConnectorMode, RuleAction, RuleUiAction } from '../../common';
|
||||
import { getSelectedActionGroup } from '../utils';
|
||||
import { ACTION_USE_AAD_TEMPLATE_FIELDS_LABEL } from '../translations';
|
||||
|
||||
export interface RuleActionsMessageProps {
|
||||
action: RuleUiAction;
|
||||
index: number;
|
||||
templateFields: ActionVariable[];
|
||||
useDefaultMessage: boolean;
|
||||
connector: ActionConnector;
|
||||
producerId: string;
|
||||
warning?: string | null;
|
||||
onParamsChange: (key: string, value: RuleActionParam) => void;
|
||||
onUseAadTemplateFieldsChange?: () => void;
|
||||
}
|
||||
|
||||
export const RuleActionsMessage = (props: RuleActionsMessageProps) => {
|
||||
const {
|
||||
action,
|
||||
index,
|
||||
templateFields,
|
||||
useDefaultMessage,
|
||||
connector,
|
||||
producerId,
|
||||
warning,
|
||||
onParamsChange,
|
||||
onUseAadTemplateFieldsChange,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
plugins: { actionTypeRegistry },
|
||||
actionsParamsErrors = {},
|
||||
selectedRuleType,
|
||||
selectedRuleTypeModel,
|
||||
connectorTypes,
|
||||
showMustacheAutocompleteSwitch,
|
||||
} = useRuleFormState();
|
||||
|
||||
const actionTypeModel = actionTypeRegistry.get(action.actionTypeId);
|
||||
|
||||
const ParamsFieldsComponent = actionTypeModel.actionParamsFields;
|
||||
|
||||
const actionsParamsError = actionsParamsErrors[action.uuid!] || {};
|
||||
|
||||
const isSystemAction = useMemo(() => {
|
||||
return connectorTypes.some((actionType) => {
|
||||
return actionType.id === action.actionTypeId && actionType.isSystemActionType;
|
||||
});
|
||||
}, [action, connectorTypes]);
|
||||
|
||||
const selectedActionGroup = useMemo(() => {
|
||||
if (isSystemAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getSelectedActionGroup({
|
||||
group: (action as RuleAction).group,
|
||||
ruleType: selectedRuleType,
|
||||
ruleTypeModel: selectedRuleTypeModel,
|
||||
});
|
||||
}, [isSystemAction, action, selectedRuleType, selectedRuleTypeModel]);
|
||||
|
||||
const defaultMessage = useMemo(() => {
|
||||
if (isSystemAction) {
|
||||
return selectedRuleTypeModel.defaultSummaryMessage;
|
||||
}
|
||||
|
||||
// if action is a summary action, show the default summary message
|
||||
return (action as RuleAction).frequency?.summary
|
||||
? selectedRuleTypeModel.defaultSummaryMessage
|
||||
: selectedActionGroup?.defaultActionMessage ?? selectedRuleTypeModel.defaultActionMessage;
|
||||
}, [isSystemAction, action, selectedRuleTypeModel, selectedActionGroup]);
|
||||
|
||||
if (!ParamsFieldsComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
<EuiFlexGroup direction="column" data-test-subj="ruleActionsMessage">
|
||||
{showMustacheAutocompleteSwitch && onUseAadTemplateFieldsChange && (
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
label={ACTION_USE_AAD_TEMPLATE_FIELDS_LABEL}
|
||||
checked={(action as RuleAction).useAlertDataForTemplate || false}
|
||||
onChange={onUseAadTemplateFieldsChange}
|
||||
data-test-subj="ruleActionsMessageUseAadTemplateFieldsSwitch"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<Suspense fallback={null}>
|
||||
<ParamsFieldsComponent
|
||||
actionParams={action.params as any}
|
||||
errors={actionsParamsError}
|
||||
index={index}
|
||||
selectedActionGroupId={selectedActionGroup?.id}
|
||||
editAction={onParamsChange}
|
||||
messageVariables={templateFields}
|
||||
defaultMessage={defaultMessage}
|
||||
useDefaultMessage={useDefaultMessage}
|
||||
actionConnector={connector}
|
||||
executionMode={ActionConnectorMode.ActionForm}
|
||||
ruleTypeId={selectedRuleType.id}
|
||||
producerId={producerId}
|
||||
/>
|
||||
{warning ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut size="s" color="warning" title={warning} />
|
||||
</>
|
||||
) : null}
|
||||
</Suspense>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
};
|
|
@ -25,9 +25,8 @@ describe('ruleActionsNotifyWhen', () => {
|
|||
frequency={frequency}
|
||||
throttle={frequency.throttle ? Number(frequency.throttle[0]) : null}
|
||||
throttleUnit={frequency.throttle ? frequency.throttle[1] : 'm'}
|
||||
onNotifyWhenChange={jest.fn()}
|
||||
onThrottleChange={jest.fn()}
|
||||
onSummaryChange={jest.fn()}
|
||||
onChange={jest.fn()}
|
||||
onUseDefaultMessage={jest.fn()}
|
||||
hasAlertsMappings={hasAlertsMappings}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -7,9 +7,15 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { css } from '@emotion/css'; // We can't use @emotion/react - this component gets used with plugins that use both styled-components and Emotion
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
RuleNotifyWhenType,
|
||||
RuleNotifyWhen,
|
||||
RuleAction,
|
||||
RuleActionFrequency,
|
||||
} from '@kbn/alerting-types';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
|
@ -29,10 +35,15 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { some, filter, map } from 'fp-ts/lib/Option';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { RuleNotifyWhenType, RuleNotifyWhen } from '@kbn/alerting-types';
|
||||
import { DEFAULT_FREQUENCY } from '../constants';
|
||||
import { getTimeOptions } from '../utils';
|
||||
import { RuleAction } from '../../common';
|
||||
|
||||
const FOR_EACH_ALERT = i18n.translate('alertsUIShared.actiActionsonNotifyWhen.forEachOption', {
|
||||
defaultMessage: 'For each alert',
|
||||
});
|
||||
const SUMMARY_OF_ALERTS = i18n.translate('alertsUIShared.actiActionsonNotifyWhen.summaryOption', {
|
||||
defaultMessage: 'Summary of alerts',
|
||||
});
|
||||
|
||||
export interface NotifyWhenSelectOptions {
|
||||
isSummaryOption?: boolean;
|
||||
|
@ -46,23 +57,26 @@ export const NOTIFY_WHEN_OPTIONS: NotifyWhenSelectOptions[] = [
|
|||
isForEachAlertOption: true,
|
||||
value: {
|
||||
value: 'onActionGroupChange',
|
||||
inputDisplay: i18n.translate('alertsUIShared.ruleForm.onActionGroupChange.display', {
|
||||
defaultMessage: 'On status changes',
|
||||
}),
|
||||
inputDisplay: i18n.translate(
|
||||
'alertsUIShared.ruleActionsNotifyWhen.onActionGroupChange.display',
|
||||
{
|
||||
defaultMessage: 'On status changes',
|
||||
}
|
||||
),
|
||||
'data-test-subj': 'onActionGroupChange',
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="On status changes"
|
||||
id="alertsUIShared.ruleForm.onActionGroupChange.label"
|
||||
id="alertsUIShared.ruleActionsNotifyWhen.onActionGroupChange.label"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Actions run if the alert status changes."
|
||||
id="alertsUIShared.ruleForm.onActionGroupChange.description"
|
||||
id="alertsUIShared.ruleActionsNotifyWhen.onActionGroupChange.description"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
@ -75,7 +89,7 @@ export const NOTIFY_WHEN_OPTIONS: NotifyWhenSelectOptions[] = [
|
|||
isForEachAlertOption: true,
|
||||
value: {
|
||||
value: 'onActiveAlert',
|
||||
inputDisplay: i18n.translate('alertsUIShared.ruleForm.onActiveAlert.display', {
|
||||
inputDisplay: i18n.translate('alertsUIShared.ruleActionsNotifyWhen.onActiveAlert.display', {
|
||||
defaultMessage: 'On check intervals',
|
||||
}),
|
||||
'data-test-subj': 'onActiveAlert',
|
||||
|
@ -84,14 +98,14 @@ export const NOTIFY_WHEN_OPTIONS: NotifyWhenSelectOptions[] = [
|
|||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="On check intervals"
|
||||
id="alertsUIShared.ruleForm.onActiveAlert.label"
|
||||
id="alertsUIShared.ruleActionsNotifyWhen.onActiveAlert.label"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Actions run if rule conditions are met."
|
||||
id="alertsUIShared.ruleForm.onActiveAlert.description"
|
||||
id="alertsUIShared.ruleActionsNotifyWhen.onActiveAlert.description"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
@ -104,23 +118,26 @@ export const NOTIFY_WHEN_OPTIONS: NotifyWhenSelectOptions[] = [
|
|||
isForEachAlertOption: true,
|
||||
value: {
|
||||
value: 'onThrottleInterval',
|
||||
inputDisplay: i18n.translate('alertsUIShared.ruleForm.onThrottleInterval.display', {
|
||||
defaultMessage: 'On custom action intervals',
|
||||
}),
|
||||
inputDisplay: i18n.translate(
|
||||
'alertsUIShared.ruleActionsNotifyWhen.onThrottleInterval.display',
|
||||
{
|
||||
defaultMessage: 'On custom action intervals',
|
||||
}
|
||||
),
|
||||
'data-test-subj': 'onThrottleInterval',
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="On custom action intervals"
|
||||
id="alertsUIShared.ruleForm.onThrottleInterval.label"
|
||||
id="alertsUIShared.ruleActionsNotifyWhen.onThrottleInterval.label"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Actions run if rule conditions are met."
|
||||
id="alertsUIShared.ruleForm.onThrottleInterval.description"
|
||||
id="alertsUIShared.ruleActionsNotifyWhen.onThrottleInterval.description"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
@ -130,18 +147,16 @@ export const NOTIFY_WHEN_OPTIONS: NotifyWhenSelectOptions[] = [
|
|||
},
|
||||
];
|
||||
|
||||
interface RuleActionsNotifyWhenProps {
|
||||
export interface RuleActionsNotifyWhenProps {
|
||||
frequency: RuleAction['frequency'];
|
||||
throttle: number | null;
|
||||
throttleUnit: string;
|
||||
onNotifyWhenChange: (notifyWhen: RuleNotifyWhenType) => void;
|
||||
onThrottleChange: (throttle: number | null, throttleUnit: string) => void;
|
||||
onSummaryChange: (summary: boolean) => void;
|
||||
hasAlertsMappings?: boolean;
|
||||
showMinimumThrottleWarning?: boolean;
|
||||
showMinimumThrottleUnitWarning?: boolean;
|
||||
notifyWhenSelectOptions?: NotifyWhenSelectOptions[];
|
||||
defaultNotifyWhenValue?: RuleNotifyWhenType;
|
||||
onChange: (frequency: RuleActionFrequency) => void;
|
||||
onUseDefaultMessage: () => void;
|
||||
}
|
||||
|
||||
export const RuleActionsNotifyWhen = ({
|
||||
|
@ -149,49 +164,26 @@ export const RuleActionsNotifyWhen = ({
|
|||
frequency = DEFAULT_FREQUENCY,
|
||||
throttle,
|
||||
throttleUnit,
|
||||
onNotifyWhenChange,
|
||||
onThrottleChange,
|
||||
onSummaryChange,
|
||||
showMinimumThrottleWarning,
|
||||
showMinimumThrottleUnitWarning,
|
||||
notifyWhenSelectOptions = NOTIFY_WHEN_OPTIONS,
|
||||
defaultNotifyWhenValue = DEFAULT_FREQUENCY.notifyWhen,
|
||||
onChange,
|
||||
onUseDefaultMessage,
|
||||
}: RuleActionsNotifyWhenProps) => {
|
||||
const [showCustomThrottleOpts, setShowCustomThrottleOpts] = useState<boolean>(false);
|
||||
const [notifyWhenValue, setNotifyWhenValue] =
|
||||
useState<RuleNotifyWhenType>(defaultNotifyWhenValue);
|
||||
|
||||
const [summaryMenuOpen, setSummaryMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (frequency.notifyWhen) {
|
||||
setNotifyWhenValue(frequency.notifyWhen);
|
||||
} else {
|
||||
// If 'notifyWhen' is not set, derive value from existence of throttle value
|
||||
setNotifyWhenValue(frequency.throttle ? RuleNotifyWhen.THROTTLE : RuleNotifyWhen.ACTIVE);
|
||||
}
|
||||
}, [frequency]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCustomThrottleOpts(notifyWhenValue === RuleNotifyWhen.THROTTLE);
|
||||
}, [notifyWhenValue]);
|
||||
const showCustomThrottleOpts = frequency?.notifyWhen === RuleNotifyWhen.THROTTLE;
|
||||
|
||||
const onNotifyWhenValueChange = useCallback(
|
||||
(newValue: RuleNotifyWhenType) => {
|
||||
onNotifyWhenChange(newValue);
|
||||
setNotifyWhenValue(newValue);
|
||||
// Calling onNotifyWhenChange and onThrottleChange at the same time interferes with the React state lifecycle
|
||||
// so wait for onNotifyWhenChange to process before calling onThrottleChange
|
||||
setTimeout(
|
||||
() =>
|
||||
onThrottleChange(
|
||||
newValue === RuleNotifyWhen.THROTTLE ? throttle ?? 1 : null,
|
||||
throttleUnit
|
||||
),
|
||||
100
|
||||
);
|
||||
const newThrottle = newValue === RuleNotifyWhen.THROTTLE ? throttle ?? 1 : null;
|
||||
onChange({
|
||||
...frequency,
|
||||
notifyWhen: newValue,
|
||||
throttle: newThrottle ? `${newThrottle}${throttleUnit}` : null,
|
||||
});
|
||||
},
|
||||
[onNotifyWhenChange, onThrottleChange, throttle, throttleUnit]
|
||||
[onChange, throttle, throttleUnit, frequency]
|
||||
);
|
||||
|
||||
const summaryNotifyWhenOptions = useMemo(
|
||||
|
@ -234,13 +226,23 @@ export const RuleActionsNotifyWhen = ({
|
|||
|
||||
const selectSummaryOption = useCallback(
|
||||
(summary: boolean) => {
|
||||
onSummaryChange(summary);
|
||||
onChange({
|
||||
summary,
|
||||
notifyWhen: selectedOptionDoesNotExist(summary)
|
||||
? getDefaultNotifyWhenOption(summary)
|
||||
: frequency.notifyWhen,
|
||||
throttle: frequency.throttle,
|
||||
});
|
||||
onUseDefaultMessage();
|
||||
setSummaryMenuOpen(false);
|
||||
if (selectedOptionDoesNotExist(summary)) {
|
||||
onNotifyWhenChange(getDefaultNotifyWhenOption(summary));
|
||||
}
|
||||
},
|
||||
[onSummaryChange, selectedOptionDoesNotExist, onNotifyWhenChange, getDefaultNotifyWhenOption]
|
||||
[
|
||||
frequency,
|
||||
onUseDefaultMessage,
|
||||
selectedOptionDoesNotExist,
|
||||
getDefaultNotifyWhenOption,
|
||||
onChange,
|
||||
]
|
||||
);
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
@ -320,7 +322,7 @@ export const RuleActionsNotifyWhen = ({
|
|||
prepend={hasAlertsMappings ? summaryOrPerRuleSelect : <></>}
|
||||
data-test-subj="notifyWhenSelect"
|
||||
options={notifyWhenOptions}
|
||||
valueOfSelected={notifyWhenValue}
|
||||
valueOfSelected={frequency.notifyWhen}
|
||||
onChange={onNotifyWhenValueChange}
|
||||
/>
|
||||
{showCustomThrottleOpts && (
|
||||
|
@ -328,7 +330,6 @@ export const RuleActionsNotifyWhen = ({
|
|||
<EuiSpacer size="s" />
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem style={{ flexGrow: 0.1 }} />
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFieldNumber
|
||||
isInvalid={showMinimumThrottleWarning}
|
||||
|
@ -349,7 +350,10 @@ export const RuleActionsNotifyWhen = ({
|
|||
map((value) => parseInt(value, 10)),
|
||||
filter((value) => !isNaN(value)),
|
||||
map((value) => {
|
||||
onThrottleChange(value, throttleUnit);
|
||||
onChange({
|
||||
...frequency,
|
||||
throttle: `${value}${throttleUnit}`,
|
||||
});
|
||||
})
|
||||
);
|
||||
}}
|
||||
|
@ -362,7 +366,10 @@ export const RuleActionsNotifyWhen = ({
|
|||
value={throttleUnit}
|
||||
options={getTimeOptions(throttle ?? 1)}
|
||||
onChange={(e) => {
|
||||
onThrottleChange(throttle, e.target.value);
|
||||
onChange({
|
||||
...frequency,
|
||||
throttle: `${throttle}${e.target.value}`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -389,10 +396,3 @@ export const RuleActionsNotifyWhen = ({
|
|||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const FOR_EACH_ALERT = i18n.translate('alertsUIShared.ruleActionsNotifyWhen.forEachOption', {
|
||||
defaultMessage: 'For each alert',
|
||||
});
|
||||
const SUMMARY_OF_ALERTS = i18n.translate('alertsUIShared.ruleActionsNotifyWhen.summaryOption', {
|
||||
defaultMessage: 'Summary of alerts',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,432 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RuleActionsSettings } from './rule_actions_settings';
|
||||
import { getAction } from '../../common/test_utils/actions_test_utils';
|
||||
import { RuleTypeModel } from '../../common';
|
||||
import { RuleType } from '@kbn/alerting-types';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { RuleActionsNotifyWhenProps } from './rule_actions_notify_when';
|
||||
import type { RuleActionsAlertsFilterProps } from './rule_actions_alerts_filter';
|
||||
import type { RuleActionsAlertsFilterTimeframeProps } from './rule_actions_alerts_filter_timeframe';
|
||||
|
||||
jest.mock('./rule_actions_notify_when', () => ({
|
||||
RuleActionsNotifyWhen: ({
|
||||
showMinimumThrottleUnitWarning,
|
||||
showMinimumThrottleWarning,
|
||||
onChange,
|
||||
onUseDefaultMessage,
|
||||
}: RuleActionsNotifyWhenProps) => (
|
||||
<div>
|
||||
RuleActionsNotifyWhen
|
||||
{showMinimumThrottleUnitWarning && <div>showMinimumThrottleUnitWarning</div>}
|
||||
{showMinimumThrottleWarning && <div>showMinimumThrottleWarning</div>}
|
||||
<button
|
||||
onClick={() =>
|
||||
onChange({
|
||||
summary: true,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
throttle: '5m',
|
||||
})
|
||||
}
|
||||
>
|
||||
RuleActionsNotifyWhenOnChange
|
||||
</button>
|
||||
<button onClick={onUseDefaultMessage}>RuleActionsNotifyWhenOnUseDefaultMessage</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_alerts_filter', () => ({
|
||||
RuleActionsAlertsFilter: ({ onChange }: RuleActionsAlertsFilterProps) => (
|
||||
<div>
|
||||
RuleActionsAlertsFilter
|
||||
<button
|
||||
onClick={() =>
|
||||
onChange({
|
||||
kql: 'test',
|
||||
filters: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
RuleActionsAlertsFilterButton
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_alerts_filter_timeframe', () => ({
|
||||
RuleActionsAlertsFilterTimeframe: ({ onChange }: RuleActionsAlertsFilterTimeframeProps) => (
|
||||
<div>
|
||||
RuleActionsAlertsFilterTimeframe
|
||||
<button
|
||||
onClick={() =>
|
||||
onChange({
|
||||
days: [1],
|
||||
timezone: 'utc',
|
||||
hours: {
|
||||
start: 'now',
|
||||
end: 'now',
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
RuleActionsAlertsFilterTimeframeButton
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
const ruleType = {
|
||||
id: '.es-query',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'testActionGroup',
|
||||
name: 'Test Action Group',
|
||||
},
|
||||
{
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'testActionGroup',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: 'recovered',
|
||||
producer: 'logs',
|
||||
authorizedConsumers: {
|
||||
alerting: { read: true, all: true },
|
||||
test: { read: true, all: true },
|
||||
stackAlerts: { read: true, all: true },
|
||||
logs: { read: true, all: true },
|
||||
},
|
||||
actionVariables: {
|
||||
params: [],
|
||||
state: [],
|
||||
},
|
||||
enabledInLicense: true,
|
||||
} as unknown as RuleType;
|
||||
|
||||
const ruleModel: RuleTypeModel = {
|
||||
id: '.es-query',
|
||||
description: 'Sample rule type model',
|
||||
iconClass: 'sampleIconClass',
|
||||
documentationUrl: 'testurl',
|
||||
validate: (params, isServerless) => ({ errors: {} }),
|
||||
ruleParamsExpression: () => <div>Expression</div>,
|
||||
defaultActionMessage: 'Sample default action message',
|
||||
defaultRecoveryMessage: 'Sample default recovery message',
|
||||
requiresAppContext: false,
|
||||
};
|
||||
|
||||
const mockOnUseDefaultMessageChange = jest.fn();
|
||||
const mockOnNotifyWhenChange = jest.fn();
|
||||
const mockOnActionGroupChange = jest.fn();
|
||||
const mockOnAlertsFilterChange = jest.fn();
|
||||
const mockOnTimeframeChange = jest.fn();
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
|
||||
|
||||
describe('ruleActionsSettings', () => {
|
||||
beforeEach(() => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
settings: {},
|
||||
},
|
||||
formData: {
|
||||
consumer: 'stackAlerts',
|
||||
schedule: { interval: '5m' },
|
||||
},
|
||||
actionErrors: {},
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
});
|
||||
useRuleFormDispatch.mockReturnValue(mockDispatch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should render correctly', () => {
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ruleActionsSettings')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ruleActionsSettingsSelectActionGroup')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render notify when component', () => {
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('RuleActionsNotifyWhen')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('showMinimumThrottleUnitWarning')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('showMinimumThrottleWarning')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render show minimum throttle unit warning', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
settings: {},
|
||||
},
|
||||
formData: {
|
||||
consumer: 'stackAlerts',
|
||||
schedule: { interval: '5h' },
|
||||
},
|
||||
actionErrors: {},
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1', {
|
||||
frequency: {
|
||||
throttle: '5m',
|
||||
summary: true,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
},
|
||||
})}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('showMinimumThrottleUnitWarning')).toBeInTheDocument();
|
||||
expect(screen.queryByText('showMinimumThrottleWarning')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render show minimum throttle warning', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
settings: {},
|
||||
},
|
||||
formData: {
|
||||
consumer: 'stackAlerts',
|
||||
schedule: { interval: '5h' },
|
||||
},
|
||||
actionErrors: {},
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1', {
|
||||
frequency: {
|
||||
throttle: '4h',
|
||||
summary: true,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
},
|
||||
})}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('showMinimumThrottleWarning')).toBeInTheDocument();
|
||||
expect(screen.queryByText('showMinimumThrottleUnitWarning')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call notifyWhen component event handlers with the correct parameters', async () => {
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('RuleActionsNotifyWhenOnChange'));
|
||||
|
||||
expect(mockOnNotifyWhenChange).toHaveBeenLastCalledWith({
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
summary: true,
|
||||
throttle: '5m',
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText('RuleActionsNotifyWhenOnUseDefaultMessage'));
|
||||
|
||||
expect(mockOnUseDefaultMessageChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should allow for selecting of action groups', async () => {
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsSettingsSelectActionGroup'));
|
||||
|
||||
await userEvent.click(screen.getByTestId('addNewActionConnectorActionGroup-testActionGroup'));
|
||||
|
||||
expect(mockOnActionGroupChange).toHaveBeenLastCalledWith('testActionGroup');
|
||||
});
|
||||
|
||||
test('should render alerts filter and filter timeframe inputs', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
settings: {},
|
||||
},
|
||||
formData: {
|
||||
consumer: 'stackAlerts',
|
||||
schedule: { interval: '5h' },
|
||||
},
|
||||
actionErrors: {},
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
selectedRuleType: {
|
||||
...ruleType,
|
||||
hasFieldsForAAD: true,
|
||||
},
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('RuleActionsAlertsFilter')).toBeInTheDocument();
|
||||
expect(screen.queryByText('RuleActionsAlertsFilterTimeframe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call filter and filter timeframe onChange', async () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
settings: {},
|
||||
},
|
||||
formData: {
|
||||
consumer: 'stackAlerts',
|
||||
schedule: { interval: '5h' },
|
||||
},
|
||||
actionErrors: {},
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
selectedRuleType: {
|
||||
...ruleType,
|
||||
hasFieldsForAAD: true,
|
||||
},
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('RuleActionsAlertsFilterButton'));
|
||||
expect(mockOnAlertsFilterChange).toHaveBeenLastCalledWith({ filters: [], kql: 'test' });
|
||||
|
||||
await userEvent.click(screen.getByText('RuleActionsAlertsFilterTimeframeButton'));
|
||||
expect(mockOnTimeframeChange).toHaveBeenLastCalledWith({
|
||||
days: [1],
|
||||
hours: { end: 'now', start: 'now' },
|
||||
timezone: 'utc',
|
||||
});
|
||||
});
|
||||
|
||||
test('should render filter query error', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
settings: {},
|
||||
},
|
||||
formData: {
|
||||
consumer: 'stackAlerts',
|
||||
schedule: { interval: '5h' },
|
||||
},
|
||||
actionsErrors: {
|
||||
'uuid-action-1': { filterQuery: ['filter query error'] },
|
||||
},
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
selectedRuleType: {
|
||||
...ruleType,
|
||||
hasFieldsForAAD: true,
|
||||
},
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('filter query error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormLabel, EuiFormRow, EuiSuperSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
AlertsFilter,
|
||||
AlertsFilterTimeframe,
|
||||
RecoveredActionGroup,
|
||||
RuleActionFrequency,
|
||||
} from '@kbn/alerting-types';
|
||||
import { AlertConsumers, ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
import { RuleAction, RuleTypeWithDescription } from '../../common';
|
||||
import {
|
||||
getActionGroups,
|
||||
getDurationNumberInItsUnit,
|
||||
getDurationUnitValue,
|
||||
getSelectedActionGroup,
|
||||
hasFieldsForAad,
|
||||
parseDuration,
|
||||
} from '../utils';
|
||||
import { DEFAULT_VALID_CONSUMERS } from '../constants';
|
||||
|
||||
import { RuleActionsNotifyWhen } from './rule_actions_notify_when';
|
||||
import { RuleActionsAlertsFilter } from './rule_actions_alerts_filter';
|
||||
import { RuleActionsAlertsFilterTimeframe } from './rule_actions_alerts_filter_timeframe';
|
||||
|
||||
const getMinimumThrottleWarnings = ({
|
||||
actionThrottle,
|
||||
actionThrottleUnit,
|
||||
minimumActionThrottle,
|
||||
minimumActionThrottleUnit,
|
||||
}: {
|
||||
actionThrottle: number | null;
|
||||
actionThrottleUnit: string;
|
||||
minimumActionThrottle: number;
|
||||
minimumActionThrottleUnit: string;
|
||||
}) => {
|
||||
try {
|
||||
if (!actionThrottle) return [false, false];
|
||||
const throttleUnitDuration = parseDuration(`1${actionThrottleUnit}`);
|
||||
const minThrottleUnitDuration = parseDuration(`1${minimumActionThrottleUnit}`);
|
||||
const boundedThrottle =
|
||||
throttleUnitDuration > minThrottleUnitDuration
|
||||
? actionThrottle
|
||||
: Math.max(actionThrottle, minimumActionThrottle);
|
||||
const boundedThrottleUnit =
|
||||
parseDuration(`${actionThrottle}${actionThrottleUnit}`) >= minThrottleUnitDuration
|
||||
? actionThrottleUnit
|
||||
: minimumActionThrottleUnit;
|
||||
return [boundedThrottle !== actionThrottle, boundedThrottleUnit !== actionThrottleUnit];
|
||||
} catch (e) {
|
||||
return [false, false];
|
||||
}
|
||||
};
|
||||
|
||||
const ACTION_GROUP_NOT_SUPPORTED = (actionGroupName: string) =>
|
||||
i18n.translate('alertsUIShared.ruleActionsSetting.actionGroupNotSupported', {
|
||||
defaultMessage: '{actionGroupName} (Not Currently Supported)',
|
||||
values: { actionGroupName },
|
||||
});
|
||||
|
||||
const ACTION_GROUP_RUN_WHEN = i18n.translate(
|
||||
'alertsUIShared.ruleActionsSetting.actionGroupRunWhen',
|
||||
{
|
||||
defaultMessage: 'Run when',
|
||||
}
|
||||
);
|
||||
|
||||
const DisabledActionGroupsByActionType: Record<string, string[]> = {
|
||||
[RecoveredActionGroup.id]: ['.jira', '.resilient'],
|
||||
};
|
||||
|
||||
const DisabledActionTypeIdsForActionGroup: Map<string, string[]> = new Map(
|
||||
Object.entries(DisabledActionGroupsByActionType)
|
||||
);
|
||||
|
||||
function isActionGroupDisabledForActionTypeId(actionGroup: string, actionTypeId: string): boolean {
|
||||
return (
|
||||
DisabledActionTypeIdsForActionGroup.has(actionGroup) &&
|
||||
DisabledActionTypeIdsForActionGroup.get(actionGroup)!.includes(actionTypeId)
|
||||
);
|
||||
}
|
||||
|
||||
const isActionGroupDisabledForActionType = (
|
||||
ruleType: RuleTypeWithDescription,
|
||||
actionGroupId: string,
|
||||
actionTypeId: string
|
||||
): boolean => {
|
||||
return isActionGroupDisabledForActionTypeId(
|
||||
actionGroupId === ruleType?.recoveryActionGroup?.id ? RecoveredActionGroup.id : actionGroupId,
|
||||
actionTypeId
|
||||
);
|
||||
};
|
||||
|
||||
const actionGroupDisplay = ({
|
||||
ruleType,
|
||||
actionGroupId,
|
||||
actionGroupName,
|
||||
actionTypeId,
|
||||
}: {
|
||||
ruleType: RuleTypeWithDescription;
|
||||
actionGroupId: string;
|
||||
actionGroupName: string;
|
||||
actionTypeId: string;
|
||||
}): string => {
|
||||
if (isActionGroupDisabledForActionType(ruleType, actionGroupId, actionTypeId)) {
|
||||
return ACTION_GROUP_NOT_SUPPORTED(actionGroupName);
|
||||
}
|
||||
return actionGroupName;
|
||||
};
|
||||
|
||||
export interface RuleActionsSettingsProps {
|
||||
action: RuleAction;
|
||||
producerId: string;
|
||||
onUseDefaultMessageChange: () => void;
|
||||
onNotifyWhenChange: (frequency: RuleActionFrequency) => void;
|
||||
onActionGroupChange: (group: string) => void;
|
||||
onAlertsFilterChange: (query?: AlertsFilter['query']) => void;
|
||||
onTimeframeChange: (timeframe?: AlertsFilterTimeframe) => void;
|
||||
}
|
||||
|
||||
export const RuleActionsSettings = (props: RuleActionsSettingsProps) => {
|
||||
const {
|
||||
action,
|
||||
producerId,
|
||||
onUseDefaultMessageChange,
|
||||
onNotifyWhenChange,
|
||||
onActionGroupChange,
|
||||
onAlertsFilterChange,
|
||||
onTimeframeChange,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
plugins: { settings },
|
||||
formData: {
|
||||
consumer,
|
||||
schedule: { interval },
|
||||
},
|
||||
actionsErrors = {},
|
||||
validConsumers = DEFAULT_VALID_CONSUMERS,
|
||||
selectedRuleType,
|
||||
selectedRuleTypeModel,
|
||||
} = useRuleFormState();
|
||||
|
||||
const actionGroups = getActionGroups({
|
||||
ruleType: selectedRuleType,
|
||||
ruleTypeModel: selectedRuleTypeModel,
|
||||
});
|
||||
|
||||
const selectedActionGroup = getSelectedActionGroup({
|
||||
group: action.group,
|
||||
ruleType: selectedRuleType,
|
||||
ruleTypeModel: selectedRuleTypeModel,
|
||||
});
|
||||
|
||||
const actionError = actionsErrors[action.uuid!] || {};
|
||||
|
||||
const showSelectActionGroup = actionGroups && selectedActionGroup && !action.frequency?.summary;
|
||||
|
||||
const intervalNumber = getDurationNumberInItsUnit(interval ?? 1);
|
||||
|
||||
const intervalUnit = getDurationUnitValue(interval);
|
||||
|
||||
const actionThrottle = action.frequency?.throttle
|
||||
? getDurationNumberInItsUnit(action.frequency.throttle)
|
||||
: null;
|
||||
|
||||
const actionThrottleUnit = action.frequency?.throttle
|
||||
? getDurationUnitValue(action.frequency?.throttle)
|
||||
: 'h';
|
||||
|
||||
const [minimumActionThrottle = -1, minimumActionThrottleUnit] = [
|
||||
intervalNumber,
|
||||
intervalUnit,
|
||||
] ?? [-1, 's'];
|
||||
|
||||
const [showMinimumThrottleWarning, showMinimumThrottleUnitWarning] = getMinimumThrottleWarnings({
|
||||
actionThrottle,
|
||||
actionThrottleUnit,
|
||||
minimumActionThrottle,
|
||||
minimumActionThrottleUnit,
|
||||
});
|
||||
|
||||
const showActionAlertsFilter =
|
||||
hasFieldsForAad({
|
||||
ruleType: selectedRuleType,
|
||||
consumer,
|
||||
validConsumers,
|
||||
}) || producerId === AlertConsumers.SIEM;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" data-test-subj="ruleActionsSettings">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexEnd">
|
||||
<EuiFlexItem>
|
||||
<RuleActionsNotifyWhen
|
||||
frequency={action.frequency}
|
||||
throttle={actionThrottle}
|
||||
throttleUnit={actionThrottleUnit}
|
||||
hasAlertsMappings={selectedRuleType.hasAlertsMappings}
|
||||
onChange={onNotifyWhenChange}
|
||||
onUseDefaultMessage={onUseDefaultMessageChange}
|
||||
showMinimumThrottleWarning={showMinimumThrottleWarning}
|
||||
showMinimumThrottleUnitWarning={showMinimumThrottleUnitWarning}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{showSelectActionGroup && (
|
||||
<EuiSuperSelect
|
||||
prepend={
|
||||
<EuiFormLabel htmlFor={`addNewActionConnectorActionGroup-${action.actionTypeId}`}>
|
||||
{ACTION_GROUP_RUN_WHEN}
|
||||
</EuiFormLabel>
|
||||
}
|
||||
data-test-subj="ruleActionsSettingsSelectActionGroup"
|
||||
fullWidth
|
||||
id={`addNewActionConnectorActionGroup-${action.actionTypeId}`}
|
||||
options={actionGroups.map(({ id: value, name }) => ({
|
||||
value,
|
||||
['data-test-subj']: `addNewActionConnectorActionGroup-${value}`,
|
||||
inputDisplay: actionGroupDisplay({
|
||||
ruleType: selectedRuleType,
|
||||
actionGroupId: value,
|
||||
actionGroupName: name,
|
||||
actionTypeId: action.actionTypeId,
|
||||
}),
|
||||
disabled: isActionGroupDisabledForActionType(
|
||||
selectedRuleType,
|
||||
value,
|
||||
action.actionTypeId
|
||||
),
|
||||
}))}
|
||||
valueOfSelected={selectedActionGroup.id}
|
||||
onChange={onActionGroupChange}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{showActionAlertsFilter && (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={actionError.filterQuery}
|
||||
isInvalid={!!actionError.filterQuery?.length}
|
||||
>
|
||||
<RuleActionsAlertsFilter
|
||||
action={action}
|
||||
onChange={onAlertsFilterChange}
|
||||
featureIds={[producerId as ValidFeatureId]}
|
||||
appName="stackAlerts"
|
||||
ruleTypeId={selectedRuleType.id}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<RuleActionsAlertsFilterTimeframe
|
||||
action={action}
|
||||
settings={settings}
|
||||
onChange={onTimeframeChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RuleType } from '@kbn/alerting-types';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { TypeRegistry } from '../../common/type_registry';
|
||||
import {
|
||||
getAction,
|
||||
getActionType,
|
||||
getActionTypeModel,
|
||||
getConnector,
|
||||
} from '../../common/test_utils/actions_test_utils';
|
||||
import { ActionTypeModel } from '../../common';
|
||||
import { RuleActionsMessageProps } from './rule_actions_message';
|
||||
import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_message', () => ({
|
||||
RuleActionsMessage: ({ onParamsChange, warning }: RuleActionsMessageProps) => (
|
||||
<div>
|
||||
RuleActionsMessage
|
||||
<button onClick={() => onParamsChange('param', { paramKey: 'someValue' })}>
|
||||
RuleActionsMessageButton
|
||||
</button>
|
||||
{warning && <div>{warning}</div>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../validation/validate_params_for_warnings', () => ({
|
||||
validateParamsForWarnings: jest.fn(),
|
||||
}));
|
||||
|
||||
const ruleType = {
|
||||
id: '.es-query',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'testActionGroup',
|
||||
name: 'Test Action Group',
|
||||
},
|
||||
{
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'testActionGroup',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: {
|
||||
id: 'recovered',
|
||||
},
|
||||
producer: 'logs',
|
||||
authorizedConsumers: {
|
||||
alerting: { read: true, all: true },
|
||||
test: { read: true, all: true },
|
||||
stackAlerts: { read: true, all: true },
|
||||
logs: { read: true, all: true },
|
||||
},
|
||||
actionVariables: {
|
||||
params: [],
|
||||
state: [],
|
||||
},
|
||||
enabledInLicense: true,
|
||||
} as unknown as RuleType;
|
||||
|
||||
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
|
||||
|
||||
const { validateParamsForWarnings } = jest.requireMock(
|
||||
'../validation/validate_params_for_warnings'
|
||||
);
|
||||
|
||||
const mockConnectors = [getConnector('1', { id: 'action-1' })];
|
||||
|
||||
const mockActionTypes = [getActionType('1')];
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
const mockValidate = jest.fn().mockResolvedValue({
|
||||
errors: {},
|
||||
});
|
||||
|
||||
describe('ruleActionsSystemActionsItem', () => {
|
||||
beforeEach(() => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('1', {
|
||||
id: 'actionType-1',
|
||||
validateParams: mockValidate,
|
||||
})
|
||||
);
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
http: {
|
||||
basePath: {
|
||||
publicBaseUrl: 'publicUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
actionsParamsErrors: {},
|
||||
selectedRuleType: ruleType,
|
||||
aadTemplateFields: [],
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockActionTypes,
|
||||
});
|
||||
useRuleFormDispatch.mockReturnValue(mockOnChange);
|
||||
validateParamsForWarnings.mockReturnValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render correctly', () => {
|
||||
render(
|
||||
<RuleActionsSystemActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ruleActionsSystemActionsItem')).toBeInTheDocument();
|
||||
expect(screen.getByText('connector-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('actionType: 1')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('ruleActionsSystemActionsItemAccordionContent')).toBeVisible();
|
||||
expect(screen.getByText('RuleActionsMessage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should be able to hide the accordion content', async () => {
|
||||
render(
|
||||
<RuleActionsSystemActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsSystemActionsItemAccordionButton'));
|
||||
|
||||
expect(screen.getByTestId('ruleActionsSystemActionsItemAccordionContent')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to delete the action', async () => {
|
||||
render(
|
||||
<RuleActionsSystemActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsSystemActionsItemDeleteActionButton'));
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { uuid: 'uuid-action-1' },
|
||||
type: 'removeAction',
|
||||
});
|
||||
});
|
||||
|
||||
test('should render error icon if error exists', async () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
http: {
|
||||
basePath: {
|
||||
publicBaseUrl: 'publicUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
actionsParamsErrors: {
|
||||
'uuid-action-1': {
|
||||
param: ['something went wrong!'],
|
||||
},
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
aadTemplateFields: [],
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockActionTypes,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsSystemActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsSystemActionsItemAccordionButton'));
|
||||
|
||||
expect(screen.getByTestId('action-group-error-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow params to be changed', async () => {
|
||||
render(
|
||||
<RuleActionsSystemActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('RuleActionsMessageButton'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { uuid: 'uuid-action-1', value: { param: { paramKey: 'someValue' } } },
|
||||
type: 'setActionParams',
|
||||
});
|
||||
expect(mockValidate).toHaveBeenCalledWith({ param: { paramKey: 'someValue' } });
|
||||
});
|
||||
|
||||
test('should set warning and error if params have errors', async () => {
|
||||
validateParamsForWarnings.mockReturnValue('warning message!');
|
||||
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('1', {
|
||||
id: 'actionType-1',
|
||||
validateParams: mockValidate.mockResolvedValue({
|
||||
errors: { paramsValue: ['something went wrong!'] },
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<RuleActionsSystemActionsItem
|
||||
action={getAction('1', { actionTypeId: 'actionType-1' })}
|
||||
index={0}
|
||||
producerId="stackAlerts"
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('RuleActionsMessageButton'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { uuid: 'uuid-action-1', value: { param: { paramKey: 'someValue' } } },
|
||||
type: 'setActionParams',
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: { errors: { paramsValue: ['something went wrong!'] }, uuid: 'uuid-action-1' },
|
||||
type: 'setActionParamsError',
|
||||
});
|
||||
|
||||
expect(screen.getByText('warning message!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty, some } from 'lodash';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiBadge,
|
||||
EuiBetaBadge,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
useEuiBackgroundColor,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { RuleActionParam, RuleSystemAction } from '@kbn/alerting-types';
|
||||
import { SavedObjectAttribute } from '@kbn/core/types';
|
||||
import { css } from '@emotion/react';
|
||||
import { useRuleFormDispatch, useRuleFormState } from '../hooks';
|
||||
import { RuleFormParamsErrors } from '../../common';
|
||||
import {
|
||||
ACTION_ERROR_TOOLTIP,
|
||||
ACTION_WARNING_TITLE,
|
||||
TECH_PREVIEW_DESCRIPTION,
|
||||
TECH_PREVIEW_LABEL,
|
||||
} from '../translations';
|
||||
import { RuleActionsMessage } from './rule_actions_message';
|
||||
import { validateParamsForWarnings } from '../validation';
|
||||
import { getAvailableActionVariables } from '../../action_variables';
|
||||
|
||||
interface RuleActionsSystemActionsItemProps {
|
||||
action: RuleSystemAction;
|
||||
index: number;
|
||||
producerId: string;
|
||||
}
|
||||
|
||||
export const RuleActionsSystemActionsItem = (props: RuleActionsSystemActionsItemProps) => {
|
||||
const { action, index, producerId } = props;
|
||||
|
||||
const {
|
||||
plugins: { actionTypeRegistry, http },
|
||||
actionsParamsErrors = {},
|
||||
selectedRuleType,
|
||||
connectorTypes,
|
||||
connectors,
|
||||
aadTemplateFields,
|
||||
} = useRuleFormState();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [storedActionParamsForAadToggle, setStoredActionParamsForAadToggle] = useState<
|
||||
Record<string, SavedObjectAttribute>
|
||||
>({});
|
||||
const [warning, setWarning] = useState<string | null>(null);
|
||||
|
||||
const subdued = useEuiBackgroundColor('subdued');
|
||||
const plain = useEuiBackgroundColor('plain');
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const dispatch = useRuleFormDispatch();
|
||||
const actionTypeModel = actionTypeRegistry.get(action.actionTypeId);
|
||||
const actionType = connectorTypes.find(({ id }) => id === action.actionTypeId)!;
|
||||
const connector = connectors.find(({ id }) => id === action.id)!;
|
||||
|
||||
const actionParamsError = actionsParamsErrors[action.uuid!] || {};
|
||||
|
||||
const availableActionVariables = useMemo(() => {
|
||||
const messageVariables = selectedRuleType.actionVariables;
|
||||
|
||||
return messageVariables
|
||||
? getAvailableActionVariables(messageVariables, undefined, undefined, true)
|
||||
: [];
|
||||
}, [selectedRuleType]);
|
||||
|
||||
const showActionGroupErrorIcon = (): boolean => {
|
||||
return !isOpen && some(actionParamsError, (error) => !isEmpty(error));
|
||||
};
|
||||
|
||||
const onDelete = (id: string) => {
|
||||
dispatch({ type: 'removeAction', payload: { uuid: id } });
|
||||
};
|
||||
|
||||
const onStoredActionParamsChange = useCallback(
|
||||
(
|
||||
aadParams: Record<string, SavedObjectAttribute>,
|
||||
params: Record<string, SavedObjectAttribute>
|
||||
) => {
|
||||
if (isEmpty(aadParams) && action.params.subAction) {
|
||||
setStoredActionParamsForAadToggle(params);
|
||||
} else {
|
||||
setStoredActionParamsForAadToggle(aadParams);
|
||||
}
|
||||
},
|
||||
[action]
|
||||
);
|
||||
|
||||
const validateActionParams = useCallback(
|
||||
async (params: RuleActionParam) => {
|
||||
const res: { errors: RuleFormParamsErrors } = await actionTypeRegistry
|
||||
.get(action.actionTypeId)
|
||||
?.validateParams(params);
|
||||
|
||||
dispatch({
|
||||
type: 'setActionParamsError',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
errors: res.errors,
|
||||
},
|
||||
});
|
||||
},
|
||||
[actionTypeRegistry, action, dispatch]
|
||||
);
|
||||
|
||||
const onParamsChange = useCallback(
|
||||
(key: string, value: RuleActionParam) => {
|
||||
const newParams = {
|
||||
...action.params,
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: 'setActionParams',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
value: newParams,
|
||||
},
|
||||
});
|
||||
setWarning(
|
||||
validateParamsForWarnings({
|
||||
value,
|
||||
publicBaseUrl: http.basePath.publicBaseUrl,
|
||||
actionVariables: availableActionVariables,
|
||||
})
|
||||
);
|
||||
validateActionParams(newParams);
|
||||
onStoredActionParamsChange(storedActionParamsForAadToggle, newParams);
|
||||
},
|
||||
[
|
||||
http,
|
||||
action,
|
||||
availableActionVariables,
|
||||
dispatch,
|
||||
validateActionParams,
|
||||
onStoredActionParamsChange,
|
||||
storedActionParamsForAadToggle,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiAccordion
|
||||
data-test-subj="ruleActionsSystemActionsItem"
|
||||
initialIsOpen
|
||||
borders="all"
|
||||
style={{
|
||||
backgroundColor: subdued,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
}}
|
||||
id={action.id}
|
||||
onToggle={setIsOpen}
|
||||
buttonProps={{
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
arrowProps={{
|
||||
css: css`
|
||||
margin-left: ${euiTheme.size.m};
|
||||
`,
|
||||
}}
|
||||
extraAction={
|
||||
<EuiButtonIcon
|
||||
data-test-subj="ruleActionsSystemActionsItemDeleteActionButton"
|
||||
style={{
|
||||
marginRight: euiTheme.size.l,
|
||||
}}
|
||||
aria-label={i18n.translate(
|
||||
'alertsUIShared.ruleActionsSystemActionsItem.deleteActionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'delete action',
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={() => onDelete(action.uuid!)}
|
||||
/>
|
||||
}
|
||||
buttonContentClassName="eui-fullWidth"
|
||||
buttonContent={
|
||||
<EuiPanel
|
||||
data-test-subj="ruleActionsSystemActionsItemAccordionButton"
|
||||
color="subdued"
|
||||
paddingSize="m"
|
||||
>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{showActionGroupErrorIcon() ? (
|
||||
<EuiToolTip content={ACTION_ERROR_TOOLTIP}>
|
||||
<EuiIcon
|
||||
data-test-subj="action-group-error-icon"
|
||||
type="warning"
|
||||
color="danger"
|
||||
size="l"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<Suspense fallback={null}>
|
||||
<EuiIcon size="l" type={actionTypeModel.iconClass} />
|
||||
</Suspense>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>{connector.name}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
<strong>{actionType?.name}</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{warning && !isOpen && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge data-test-subj="warning-badge" iconType="warning" color="warning">
|
||||
{ACTION_WARNING_TITLE}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{actionTypeModel.isExperimental && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
alignment="middle"
|
||||
data-test-subj="ruleActionsSystemActionsItemBetaBadge"
|
||||
label={TECH_PREVIEW_LABEL}
|
||||
tooltipContent={TECH_PREVIEW_DESCRIPTION}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
data-test-subj="ruleActionsSystemActionsItemAccordionContent"
|
||||
direction="column"
|
||||
style={{
|
||||
padding: euiTheme.size.l,
|
||||
backgroundColor: plain,
|
||||
}}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<RuleActionsMessage
|
||||
useDefaultMessage
|
||||
action={action}
|
||||
index={index}
|
||||
connector={connector}
|
||||
producerId={producerId}
|
||||
warning={warning}
|
||||
templateFields={aadTemplateFields}
|
||||
onParamsChange={onParamsChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiAccordion>
|
||||
);
|
||||
};
|
|
@ -52,15 +52,6 @@ describe('RuleConsumerSelection', () => {
|
|||
expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should display nothing if there is only 1 consumer to select', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
multiConsumerSelection: null,
|
||||
});
|
||||
render(<RuleConsumerSelection validConsumers={['stackAlerts']} />);
|
||||
|
||||
expect(screen.queryByTestId('ruleConsumerSelection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to select logs and call onChange', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
multiConsumerSelection: null,
|
||||
|
|
|
@ -91,10 +91,6 @@ export const RuleConsumerSelection = (props: RuleConsumerSelectionProps) => {
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
if (validConsumers.length <= 1 || validConsumers.includes(AlertConsumers.OBSERVABILITY)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
|
|
|
@ -169,6 +169,58 @@ describe('Rule Definition', () => {
|
|||
expect(screen.queryByTestId('ruleConsumerSelection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Hides consumer selection if there is only 1 consumer to select', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins,
|
||||
formData: {
|
||||
id: 'test-id',
|
||||
params: {},
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
alertDelay: {
|
||||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
canShowConsumerSelect: true,
|
||||
validConsumers: ['logs'],
|
||||
});
|
||||
|
||||
render(<RuleDefinition />);
|
||||
|
||||
expect(screen.queryByTestId('ruleConsumerSelection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Hides consumer selection if valid consumers contain observability', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins,
|
||||
formData: {
|
||||
id: 'test-id',
|
||||
params: {},
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
alertDelay: {
|
||||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
canShowConsumerSelect: true,
|
||||
validConsumers: ['logs', 'observability'],
|
||||
});
|
||||
|
||||
render(<RuleDefinition />);
|
||||
|
||||
expect(screen.queryByTestId('ruleConsumerSelection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Can toggle advanced options', async () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins,
|
||||
|
|
|
@ -22,7 +22,11 @@ import {
|
|||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiErrorBoundary,
|
||||
useEuiTheme,
|
||||
COLOR_MODES_STANDARD,
|
||||
} from '@elastic/eui';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
DOC_LINK_TITLE,
|
||||
LOADING_RULE_TYPE_PARAMS_TITLE,
|
||||
|
@ -56,6 +60,7 @@ export const RuleDefinition = () => {
|
|||
canShowConsumerSelection = false,
|
||||
} = useRuleFormState();
|
||||
|
||||
const { colorMode } = useEuiTheme();
|
||||
const dispatch = useRuleFormDispatch();
|
||||
|
||||
const { charts, data, dataViews, unifiedSearch, docLinks } = plugins;
|
||||
|
@ -81,6 +86,12 @@ export const RuleDefinition = () => {
|
|||
if (!authorizedConsumers.length) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
authorizedConsumers.length <= 1 ||
|
||||
authorizedConsumers.includes(AlertConsumers.OBSERVABILITY)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
selectedRuleTypeModel.id && MULTI_CONSUMER_RULE_TYPE_IDS.includes(selectedRuleTypeModel.id)
|
||||
);
|
||||
|
@ -174,24 +185,26 @@ export const RuleDefinition = () => {
|
|||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiErrorBoundary>
|
||||
<RuleParamsExpressionComponent
|
||||
id={id}
|
||||
ruleParams={params}
|
||||
ruleInterval={schedule.interval}
|
||||
ruleThrottle={''}
|
||||
alertNotifyWhen={notifyWhen || 'onActionGroupChange'}
|
||||
errors={paramsErrors || {}}
|
||||
setRuleParams={onSetRuleParams}
|
||||
setRuleProperty={onSetRuleProperty}
|
||||
defaultActionGroupId={selectedRuleType.defaultActionGroupId}
|
||||
actionGroups={selectedRuleType.actionGroups}
|
||||
metadata={metadata}
|
||||
charts={charts}
|
||||
data={data}
|
||||
dataViews={dataViews}
|
||||
unifiedSearch={unifiedSearch}
|
||||
onChangeMetaData={onChangeMetaData}
|
||||
/>
|
||||
<EuiThemeProvider darkMode={colorMode === COLOR_MODES_STANDARD.dark}>
|
||||
<RuleParamsExpressionComponent
|
||||
id={id}
|
||||
ruleParams={params}
|
||||
ruleInterval={schedule.interval}
|
||||
ruleThrottle={''}
|
||||
alertNotifyWhen={notifyWhen || 'onActionGroupChange'}
|
||||
errors={paramsErrors || {}}
|
||||
setRuleParams={onSetRuleParams}
|
||||
setRuleProperty={onSetRuleProperty}
|
||||
defaultActionGroupId={selectedRuleType.defaultActionGroupId}
|
||||
actionGroups={selectedRuleType.actionGroups}
|
||||
metadata={metadata}
|
||||
charts={charts}
|
||||
data={data}
|
||||
dataViews={dataViews}
|
||||
unifiedSearch={unifiedSearch}
|
||||
onChangeMetaData={onChangeMetaData}
|
||||
/>
|
||||
</EuiThemeProvider>
|
||||
</EuiErrorBoundary>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -12,3 +12,4 @@ export * from './rule_form_circuit_breaker_error';
|
|||
export * from './rule_form_resolve_rule_error';
|
||||
export * from './rule_form_rule_type_error';
|
||||
export * from './rule_form_error_prompt_wrapper';
|
||||
export * from './rule_form_action_permission_error';
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
RULE_FORM_PAGE_RULE_ACTIONS_NO_PERMISSION_TITLE,
|
||||
RULE_FORM_PAGE_RULE_ACTIONS_NO_PERMISSION_DESCRIPTION,
|
||||
} from '../translations';
|
||||
|
||||
export const RuleFormActionPermissionError = () => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="error"
|
||||
color="danger"
|
||||
title={
|
||||
<EuiText color="default">
|
||||
<h2>{RULE_FORM_PAGE_RULE_ACTIONS_NO_PERMISSION_TITLE}</h2>
|
||||
</EuiText>
|
||||
}
|
||||
body={
|
||||
<EuiText>
|
||||
<p>{RULE_FORM_PAGE_RULE_ACTIONS_NO_PERMISSION_DESCRIPTION}</p>
|
||||
</EuiText>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -11,6 +11,7 @@ import React, { useReducer } from 'react';
|
|||
import { act, renderHook } from '@testing-library/react-hooks/dom';
|
||||
import { ruleFormStateReducer } from './rule_form_state_reducer';
|
||||
import { RuleFormState } from '../types';
|
||||
import { getAction } from '../../common/test_utils/actions_test_utils';
|
||||
|
||||
jest.mock('../validation/validate_form', () => ({
|
||||
validateRuleBase: jest.fn(),
|
||||
|
@ -63,6 +64,7 @@ const initialState: RuleFormState = {
|
|||
formData: {
|
||||
name: 'test-rule',
|
||||
tags: [],
|
||||
actions: [],
|
||||
params: {
|
||||
paramsValue: 'value-1',
|
||||
},
|
||||
|
@ -74,6 +76,9 @@ const initialState: RuleFormState = {
|
|||
selectedRuleType: indexThresholdRuleType,
|
||||
selectedRuleTypeModel: indexThresholdRuleTypeModel,
|
||||
multiConsumerSelection: 'stackAlerts',
|
||||
connectors: [],
|
||||
connectorTypes: [],
|
||||
aadTemplateFields: [],
|
||||
};
|
||||
|
||||
describe('ruleFormStateReducer', () => {
|
||||
|
@ -97,6 +102,7 @@ describe('ruleFormStateReducer', () => {
|
|||
params: {
|
||||
test: 'hello',
|
||||
},
|
||||
actions: [],
|
||||
schedule: { interval: '2m' },
|
||||
consumer: 'logs',
|
||||
};
|
||||
|
@ -345,4 +351,218 @@ describe('ruleFormStateReducer', () => {
|
|||
expect(validateRuleBase).not.toHaveBeenCalled();
|
||||
expect(validateRuleParams).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('addAction works correctly', () => {
|
||||
const { result } = renderHook(() => useReducer(ruleFormStateReducer, initialState));
|
||||
|
||||
const dispatch = result.current[1];
|
||||
|
||||
const action = getAction('1');
|
||||
|
||||
act(() => {
|
||||
dispatch({
|
||||
type: 'addAction',
|
||||
payload: action,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current[0].formData.actions).toEqual([action]);
|
||||
|
||||
expect(validateRuleBase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({ actions: [action] }),
|
||||
})
|
||||
);
|
||||
expect(validateRuleParams).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({ actions: [action] }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('removeAction works correctly', () => {
|
||||
const action1 = getAction('1');
|
||||
const action2 = getAction('2');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useReducer(ruleFormStateReducer, {
|
||||
...initialState,
|
||||
formData: {
|
||||
...initialState.formData,
|
||||
actions: [action1, action2],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const dispatch = result.current[1];
|
||||
|
||||
act(() => {
|
||||
dispatch({
|
||||
type: 'removeAction',
|
||||
payload: {
|
||||
uuid: action1.uuid!,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current[0].formData.actions).toEqual([action2]);
|
||||
|
||||
expect(validateRuleBase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({ actions: [action2] }),
|
||||
})
|
||||
);
|
||||
expect(validateRuleParams).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({ actions: [action2] }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('setActionProperty works correctly', () => {
|
||||
const action = getAction('1');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useReducer(ruleFormStateReducer, {
|
||||
...initialState,
|
||||
formData: {
|
||||
...initialState.formData,
|
||||
actions: [action],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const dispatch = result.current[1];
|
||||
|
||||
act(() => {
|
||||
dispatch({
|
||||
type: 'setActionProperty',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
key: 'params',
|
||||
value: {
|
||||
test: 'value',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const updatedAction = {
|
||||
...action,
|
||||
params: {
|
||||
test: 'value',
|
||||
},
|
||||
};
|
||||
|
||||
expect(result.current[0].formData.actions).toEqual([updatedAction]);
|
||||
|
||||
expect(validateRuleBase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({ actions: [updatedAction] }),
|
||||
})
|
||||
);
|
||||
expect(validateRuleParams).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({ actions: [updatedAction] }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('setActionParams works correctly', () => {
|
||||
const action = getAction('1');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useReducer(ruleFormStateReducer, {
|
||||
...initialState,
|
||||
formData: {
|
||||
...initialState.formData,
|
||||
actions: [action],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const dispatch = result.current[1];
|
||||
|
||||
act(() => {
|
||||
dispatch({
|
||||
type: 'setActionParams',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
value: {
|
||||
test: 'value',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const updatedAction = {
|
||||
...action,
|
||||
params: {
|
||||
test: 'value',
|
||||
},
|
||||
};
|
||||
|
||||
expect(result.current[0].formData.actions).toEqual([updatedAction]);
|
||||
|
||||
expect(validateRuleBase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({ actions: [updatedAction] }),
|
||||
})
|
||||
);
|
||||
expect(validateRuleParams).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({ actions: [updatedAction] }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('setActionError works correctly', () => {
|
||||
const { result } = renderHook(() => useReducer(ruleFormStateReducer, initialState));
|
||||
|
||||
const dispatch = result.current[1];
|
||||
|
||||
const action = getAction('1');
|
||||
|
||||
act(() => {
|
||||
dispatch({
|
||||
type: 'setActionError',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
errors: { ['property' as string]: 'something went wrong' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current[0].actionsErrors).toEqual({
|
||||
'uuid-action-1': { property: 'something went wrong' },
|
||||
});
|
||||
|
||||
expect(validateRuleBase).not.toHaveBeenCalled();
|
||||
expect(validateRuleParams).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('setActionParamsError works correctly', () => {
|
||||
const { result } = renderHook(() => useReducer(ruleFormStateReducer, initialState));
|
||||
|
||||
const dispatch = result.current[1];
|
||||
|
||||
const action = getAction('1');
|
||||
|
||||
act(() => {
|
||||
dispatch({
|
||||
type: 'setActionParamsError',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
errors: { ['property' as string]: 'something went wrong' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current[0].actionsParamsErrors).toEqual({
|
||||
'uuid-action-1': { property: 'something went wrong' },
|
||||
});
|
||||
|
||||
expect(validateRuleBase).not.toHaveBeenCalled();
|
||||
expect(validateRuleParams).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { RuleActionParams } from '@kbn/alerting-types';
|
||||
import { omit } from 'lodash';
|
||||
import { RuleFormActionsErrors, RuleFormParamsErrors, RuleUiAction } from '../../common';
|
||||
import { RuleFormData, RuleFormState } from '../types';
|
||||
import { validateRuleBase, validateRuleParams } from '../validation';
|
||||
|
||||
|
@ -64,6 +67,45 @@ export type RuleFormStateReducerAction =
|
|||
| {
|
||||
type: 'setMetadata';
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: 'addAction';
|
||||
payload: RuleUiAction;
|
||||
}
|
||||
| {
|
||||
type: 'removeAction';
|
||||
payload: {
|
||||
uuid: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'setActionProperty';
|
||||
payload: {
|
||||
uuid: string;
|
||||
key: string;
|
||||
value: unknown;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'setActionParams';
|
||||
payload: {
|
||||
uuid: string;
|
||||
value: RuleActionParams;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'setActionError';
|
||||
payload: {
|
||||
uuid: string;
|
||||
errors: RuleFormActionsErrors;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'setActionParamsError';
|
||||
payload: {
|
||||
uuid: string;
|
||||
errors: RuleFormParamsErrors;
|
||||
};
|
||||
};
|
||||
|
||||
const getUpdateWithValidation =
|
||||
|
@ -189,6 +231,101 @@ export const ruleFormStateReducer = (
|
|||
metadata: payload,
|
||||
};
|
||||
}
|
||||
case 'addAction': {
|
||||
const { payload } = action;
|
||||
return updateWithValidation(() => ({
|
||||
...formData,
|
||||
actions: [...formData.actions, payload],
|
||||
}));
|
||||
}
|
||||
case 'removeAction': {
|
||||
const {
|
||||
payload: { uuid },
|
||||
} = action;
|
||||
return {
|
||||
...ruleFormState,
|
||||
...updateWithValidation(() => ({
|
||||
...formData,
|
||||
actions: formData.actions.filter((existingAction) => existingAction.uuid !== uuid),
|
||||
})),
|
||||
...(ruleFormState.actionsErrors
|
||||
? {
|
||||
actionsErrors: omit(ruleFormState.actionsErrors, uuid),
|
||||
}
|
||||
: {}),
|
||||
...(ruleFormState.actionsParamsErrors
|
||||
? {
|
||||
actionsParamsErrors: omit(ruleFormState.actionsParamsErrors, uuid),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
case 'setActionProperty': {
|
||||
const {
|
||||
payload: { uuid, key, value },
|
||||
} = action;
|
||||
return updateWithValidation(() => ({
|
||||
...formData,
|
||||
actions: formData.actions.map((existingAction) => {
|
||||
if (existingAction.uuid === uuid) {
|
||||
return {
|
||||
...existingAction,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
return existingAction;
|
||||
}),
|
||||
}));
|
||||
}
|
||||
case 'setActionParams': {
|
||||
const {
|
||||
payload: { uuid, value },
|
||||
} = action;
|
||||
return updateWithValidation(() => ({
|
||||
...formData,
|
||||
actions: formData.actions.map((existingAction) => {
|
||||
if (existingAction.uuid === uuid) {
|
||||
return {
|
||||
...existingAction,
|
||||
params: value,
|
||||
};
|
||||
}
|
||||
return existingAction;
|
||||
}),
|
||||
}));
|
||||
}
|
||||
case 'setActionError': {
|
||||
const {
|
||||
payload: { uuid, errors },
|
||||
} = action;
|
||||
const newActionsError = {
|
||||
...(ruleFormState.actionsErrors || {})[uuid],
|
||||
...errors,
|
||||
};
|
||||
return {
|
||||
...ruleFormState,
|
||||
actionsErrors: {
|
||||
...ruleFormState.actionsErrors,
|
||||
[uuid]: newActionsError,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'setActionParamsError': {
|
||||
const {
|
||||
payload: { uuid, errors },
|
||||
} = action;
|
||||
const newActionsParamsError = {
|
||||
...(ruleFormState.actionsParamsErrors || {})[uuid],
|
||||
...errors,
|
||||
};
|
||||
return {
|
||||
...ruleFormState,
|
||||
actionsParamsErrors: {
|
||||
...ruleFormState.actionsParamsErrors,
|
||||
[uuid]: newActionsParamsError,
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return ruleFormState;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ const formDataMock: RuleFormData = {
|
|||
index: ['.kibana'],
|
||||
timeField: 'alert.executionStatus.lastExecutionDate',
|
||||
},
|
||||
actions: [],
|
||||
consumer: 'stackAlerts',
|
||||
schedule: { interval: '1m' },
|
||||
tags: [],
|
||||
|
@ -64,12 +65,22 @@ useRuleFormState.mockReturnValue({
|
|||
plugins: {
|
||||
application: {
|
||||
navigateToUrl,
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
save: true,
|
||||
execute: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
baseErrors: {},
|
||||
paramsErrors: {},
|
||||
multiConsumerSelection: 'logs',
|
||||
formData: formDataMock,
|
||||
connectors: [],
|
||||
connectorTypes: [],
|
||||
aadTemplateFields: [],
|
||||
});
|
||||
|
||||
const onSave = jest.fn();
|
||||
|
|
|
@ -51,6 +51,8 @@ export const RulePage = (props: RulePageProps) => {
|
|||
multiConsumerSelection,
|
||||
} = useRuleFormState();
|
||||
|
||||
const canReadConnectors = !!application.capabilities.actions?.show;
|
||||
|
||||
const styles = useEuiBackgroundColorCSS().transparent;
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
|
@ -64,22 +66,31 @@ export const RulePage = (props: RulePageProps) => {
|
|||
});
|
||||
}, [onSave, formData, multiConsumerSelection]);
|
||||
|
||||
const actionComponent = useMemo(() => {
|
||||
if (canReadConnectors) {
|
||||
return [
|
||||
{
|
||||
title: RULE_FORM_PAGE_RULE_ACTIONS_TITLE,
|
||||
children: (
|
||||
<>
|
||||
<RuleActions />
|
||||
<EuiSpacer />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}, [canReadConnectors]);
|
||||
|
||||
const steps: EuiStepsProps['steps'] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: RULE_FORM_PAGE_RULE_DEFINITION_TITLE,
|
||||
children: <RuleDefinition />,
|
||||
},
|
||||
{
|
||||
title: RULE_FORM_PAGE_RULE_ACTIONS_TITLE,
|
||||
children: (
|
||||
<>
|
||||
<RuleActions onClick={() => {}} />
|
||||
<EuiSpacer />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
...actionComponent,
|
||||
{
|
||||
title: RULE_FORM_PAGE_RULE_DETAILS_TITLE,
|
||||
children: (
|
||||
|
@ -91,7 +102,7 @@ export const RulePage = (props: RulePageProps) => {
|
|||
),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
}, [actionComponent]);
|
||||
|
||||
return (
|
||||
<EuiPageTemplate grow bottomBorder offset={0} css={styles}>
|
||||
|
|
|
@ -35,6 +35,9 @@ hasRuleErrors.mockReturnValue(false);
|
|||
useRuleFormState.mockReturnValue({
|
||||
baseErrors: {},
|
||||
paramsErrors: {},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
|
||||
describe('rulePageFooter', () => {
|
||||
|
|
|
@ -33,14 +33,31 @@ export const RulePageFooter = (props: RulePageFooterProps) => {
|
|||
|
||||
const { isEdit = false, isSaving = false, onCancel, onSave } = props;
|
||||
|
||||
const { baseErrors, paramsErrors } = useRuleFormState();
|
||||
const {
|
||||
formData: { actions },
|
||||
connectors,
|
||||
baseErrors = {},
|
||||
paramsErrors = {},
|
||||
actionsErrors = {},
|
||||
actionsParamsErrors = {},
|
||||
} = useRuleFormState();
|
||||
|
||||
const hasErrors = useMemo(() => {
|
||||
return hasRuleErrors({
|
||||
baseErrors: baseErrors || {},
|
||||
paramsErrors: paramsErrors || {},
|
||||
const hasBrokenConnectors = actions.some((action) => {
|
||||
return !connectors.find((connector) => connector.id === action.id);
|
||||
});
|
||||
}, [baseErrors, paramsErrors]);
|
||||
|
||||
if (hasBrokenConnectors) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hasRuleErrors({
|
||||
baseErrors,
|
||||
paramsErrors,
|
||||
actionsErrors,
|
||||
actionsParamsErrors,
|
||||
});
|
||||
}, [actions, connectors, baseErrors, paramsErrors, actionsErrors, actionsParamsErrors]);
|
||||
|
||||
const saveButtonText = useMemo(() => {
|
||||
if (isEdit) {
|
||||
|
@ -59,11 +76,13 @@ export const RulePageFooter = (props: RulePageFooterProps) => {
|
|||
|
||||
const onSaveClick = useCallback(() => {
|
||||
if (isEdit) {
|
||||
onSave();
|
||||
} else {
|
||||
setShowCreateConfirmation(true);
|
||||
return onSave();
|
||||
}
|
||||
}, [isEdit, onSave]);
|
||||
if (actions.length === 0) {
|
||||
return setShowCreateConfirmation(true);
|
||||
}
|
||||
onSave();
|
||||
}, [actions, isEdit, onSave]);
|
||||
|
||||
const onCreateConfirmClick = useCallback(() => {
|
||||
setShowCreateConfirmation(false);
|
||||
|
|
|
@ -35,6 +35,7 @@ const formData: RuleFormData = {
|
|||
index: ['.kibana'],
|
||||
timeField: 'created_at',
|
||||
},
|
||||
actions: [],
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
schedule: { interval: '1m' },
|
||||
|
|
|
@ -25,7 +25,7 @@ import { BASE_ALERTING_API_PATH } from '../../common/constants';
|
|||
import { RuleFormData } from '../types';
|
||||
import {
|
||||
CreateRuleBody,
|
||||
UPDATE_FIELDS,
|
||||
UPDATE_FIELDS_WITH_ACTIONS,
|
||||
UpdateRuleBody,
|
||||
transformCreateRuleBody,
|
||||
transformUpdateRuleBody,
|
||||
|
@ -41,7 +41,7 @@ const stringifyBodyRequest = ({
|
|||
}): string => {
|
||||
try {
|
||||
const request = isEdit
|
||||
? transformUpdateRuleBody(pick(formData, UPDATE_FIELDS) as UpdateRuleBody)
|
||||
? transformUpdateRuleBody(pick(formData, UPDATE_FIELDS_WITH_ACTIONS) as UpdateRuleBody)
|
||||
: transformCreateRuleBody(omit(formData, 'id') as CreateRuleBody);
|
||||
return JSON.stringify(request, null, 2);
|
||||
} catch {
|
||||
|
|
|
@ -337,7 +337,7 @@ export const HEALTH_CHECK_ACTION_TEXT = i18n.translate('alertsUIShared.healthChe
|
|||
export const RULE_FORM_ROUTE_PARAMS_ERROR_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleForm.routeParamsErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Unable to load rule form.',
|
||||
defaultMessage: 'Unable to load rule form',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -351,7 +351,7 @@ export const RULE_FORM_ROUTE_PARAMS_ERROR_TEXT = i18n.translate(
|
|||
export const RULE_FORM_RULE_TYPE_NOT_FOUND_ERROR_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleForm.ruleTypeNotFoundErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Unable to load rule type.',
|
||||
defaultMessage: 'Unable to load rule type',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -374,7 +374,7 @@ export const RULE_FORM_RULE_NOT_FOUND_ERROR_TEXT = i18n.translate(
|
|||
'alertsUIShared.ruleForm.ruleNotFoundErrorText',
|
||||
{
|
||||
defaultMessage:
|
||||
'There was an error loading the rule. Please ensure you have access to the rule selected.',
|
||||
'There was an error loading the rule. Please ensure the rule exists and you have access to the rule selected.',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -458,6 +458,20 @@ export const RULE_FORM_PAGE_RULE_ACTIONS_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const RULE_FORM_PAGE_RULE_ACTIONS_NO_PERMISSION_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleForm.ruleActionsNoPermissionTitle',
|
||||
{
|
||||
defaultMessage: 'Actions and connectors privileges missing',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_FORM_PAGE_RULE_ACTIONS_NO_PERMISSION_DESCRIPTION = i18n.translate(
|
||||
'alertsUIShared.ruleForm.ruleActionsNoPermissionDescription',
|
||||
{
|
||||
defaultMessage: 'You must have read access to actions and connectors to edit rules.',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_FORM_PAGE_RULE_DETAILS_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleForm.ruleDetailsTitle',
|
||||
{
|
||||
|
@ -468,3 +482,92 @@ export const RULE_FORM_PAGE_RULE_DETAILS_TITLE = i18n.translate(
|
|||
export const RULE_FORM_RETURN_TITLE = i18n.translate('alertsUIShared.ruleForm.returnTitle', {
|
||||
defaultMessage: 'Return',
|
||||
});
|
||||
|
||||
export const MODAL_SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'alertsUIShared.ruleForm.modalSearchPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search',
|
||||
}
|
||||
);
|
||||
|
||||
export const MODAL_SEARCH_CLEAR_FILTERS_TEXT = i18n.translate(
|
||||
'alertsUIShared.ruleForm.modalSearchClearFiltersText',
|
||||
{
|
||||
defaultMessage: 'Clear filters',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_TYPE_MODAL_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleForm.actionTypeModalTitle',
|
||||
{
|
||||
defaultMessage: 'Select connector',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_TYPE_MODAL_FILTER_ALL = i18n.translate(
|
||||
'alertsUIShared.ruleForm.actionTypeModalFilterAll',
|
||||
{
|
||||
defaultMessage: 'All',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_TYPE_MODAL_EMPTY_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleForm.actionTypeModalEmptyTitle',
|
||||
{
|
||||
defaultMessage: 'No connectors found',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_TYPE_MODAL_EMPTY_TEXT = i18n.translate(
|
||||
'alertsUIShared.ruleForm.actionTypeModalEmptyText',
|
||||
{
|
||||
defaultMessage: 'Try a different search or change your filter settings.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ERROR_TOOLTIP = i18n.translate(
|
||||
'alertsUIShared.ruleActionsItem.actionErrorToolTip',
|
||||
{
|
||||
defaultMessage: 'Action contains errors.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_WARNING_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleActionsItem.actionWarningsTitle',
|
||||
{
|
||||
defaultMessage: '1 warning',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_UNABLE_TO_LOAD_CONNECTOR_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleActionsItem.actionUnableToLoadConnectorTitle',
|
||||
{
|
||||
defaultMessage: 'Unable to find connector',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_UNABLE_TO_LOAD_CONNECTOR_DESCRIPTION = i18n.translate(
|
||||
'alertsUIShared.ruleActionsItem.actionUnableToLoadConnectorTitle',
|
||||
{
|
||||
defaultMessage: `Create a connector and try again. If you can't create a connector, contact your system administrator.`,
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_USE_AAD_TEMPLATE_FIELDS_LABEL = i18n.translate(
|
||||
'alertsUIShared.ruleActionsItem.actionUseAadTemplateFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Use template fields from alerts index',
|
||||
}
|
||||
);
|
||||
|
||||
export const TECH_PREVIEW_LABEL = i18n.translate('alertsUIShared.technicalPreviewBadgeLabel', {
|
||||
defaultMessage: 'Technical preview',
|
||||
});
|
||||
|
||||
export const TECH_PREVIEW_DESCRIPTION = i18n.translate(
|
||||
'alertsUIShared.technicalPreviewBadgeDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -17,16 +17,23 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
|
|||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import { RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
import { ActionType } from '@kbn/actions-types';
|
||||
import { ActionVariable } from '@kbn/alerting-types';
|
||||
import {
|
||||
ActionConnector,
|
||||
ActionTypeRegistryContract,
|
||||
MinimumScheduleInterval,
|
||||
Rule,
|
||||
RuleFormActionsErrors,
|
||||
RuleFormBaseErrors,
|
||||
RuleFormParamsErrors,
|
||||
RuleTypeModel,
|
||||
RuleTypeParams,
|
||||
RuleTypeRegistryContract,
|
||||
RuleTypeWithDescription,
|
||||
RuleUiAction,
|
||||
} from '../common/types';
|
||||
|
||||
export interface RuleFormData<Params extends RuleTypeParams = RuleTypeParams> {
|
||||
|
@ -35,6 +42,7 @@ export interface RuleFormData<Params extends RuleTypeParams = RuleTypeParams> {
|
|||
params: Rule<Params>['params'];
|
||||
schedule: Rule<Params>['schedule'];
|
||||
consumer: Rule<Params>['consumer'];
|
||||
actions: RuleUiAction[];
|
||||
alertDelay?: Rule<Params>['alertDelay'];
|
||||
notifyWhen?: Rule<Params>['notifyWhen'];
|
||||
ruleTypeId?: Rule<Params>['ruleTypeId'];
|
||||
|
@ -45,24 +53,32 @@ export interface RuleFormPlugins {
|
|||
i18n: I18nStart;
|
||||
theme: ThemeServiceStart;
|
||||
application: ApplicationStart;
|
||||
notification: NotificationsStart;
|
||||
notifications: NotificationsStart;
|
||||
charts: ChartsPluginSetup;
|
||||
settings: SettingsStart;
|
||||
data: DataPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
docLinks: DocLinksStart;
|
||||
ruleTypeRegistry: RuleTypeRegistryContract;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
}
|
||||
|
||||
export interface RuleFormState<Params extends RuleTypeParams = RuleTypeParams> {
|
||||
id?: string;
|
||||
formData: RuleFormData<Params>;
|
||||
plugins: RuleFormPlugins;
|
||||
connectors: ActionConnector[];
|
||||
connectorTypes: ActionType[];
|
||||
aadTemplateFields: ActionVariable[];
|
||||
baseErrors?: RuleFormBaseErrors;
|
||||
paramsErrors?: RuleFormParamsErrors;
|
||||
actionsErrors?: Record<string, RuleFormActionsErrors>;
|
||||
actionsParamsErrors?: Record<string, RuleFormParamsErrors>;
|
||||
selectedRuleType: RuleTypeWithDescription;
|
||||
selectedRuleTypeModel: RuleTypeModel<Params>;
|
||||
multiConsumerSelection?: RuleCreationValidConsumer | null;
|
||||
showMustacheAutocompleteSwitch?: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
minimumScheduleInterval?: MinimumScheduleInterval;
|
||||
canShowConsumerSelection?: boolean;
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ActionType, PreConfiguredActionConnector } from '../../types';
|
||||
import { ActionType } from '@kbn/actions-types';
|
||||
import {
|
||||
checkActionTypeEnabled,
|
||||
checkActionFormActionTypeEnabled,
|
||||
} from './check_action_type_enabled';
|
||||
import { PreConfiguredActionConnector } from '../../common';
|
||||
|
||||
describe('checkActionTypeEnabled', () => {
|
||||
test(`returns isEnabled:true when action type isn't provided`, async () => {
|
||||
|
@ -65,7 +68,7 @@ describe('checkActionTypeEnabled', () => {
|
|||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="View license options"
|
||||
id="xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseLinkTitle"
|
||||
id="alertsUIShared.licenseCheck.actionTypeDisabledByLicenseLinkTitle"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiCard>,
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ActionType } from '@kbn/actions-types';
|
||||
import { configurationCheckResult, getLicenseCheckResult } from './get_license_check_result';
|
||||
import { ActionConnector } from '../../common';
|
||||
import './check_action_type_enabled.scss';
|
||||
|
||||
export interface IsEnabledResult {
|
||||
isEnabled: true;
|
||||
}
|
||||
export interface IsDisabledResult {
|
||||
isEnabled: false;
|
||||
message: string;
|
||||
messageCard: JSX.Element;
|
||||
}
|
||||
|
||||
export const checkActionTypeEnabled = (
|
||||
actionType?: ActionType
|
||||
): IsEnabledResult | IsDisabledResult => {
|
||||
if (actionType?.enabledInLicense === false) {
|
||||
return getLicenseCheckResult(actionType);
|
||||
}
|
||||
|
||||
if (actionType?.enabledInConfig === false) {
|
||||
return configurationCheckResult;
|
||||
}
|
||||
|
||||
return { isEnabled: true };
|
||||
};
|
||||
|
||||
export const 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 };
|
||||
};
|
|
@ -75,5 +75,5 @@ export const getInitialMultiConsumer = ({
|
|||
}
|
||||
|
||||
// All else fails, just use the first valid consumer
|
||||
return validConsumers[0];
|
||||
return validConsumers[0] || null;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiCard, EuiLink } from '@elastic/eui';
|
||||
import { ActionType } from '@kbn/actions-types';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { VIEW_LICENSE_OPTIONS_LINK } from '../../common/constants';
|
||||
|
||||
export const getLicenseCheckResult = (actionType: ActionType) => {
|
||||
return {
|
||||
isEnabled: false,
|
||||
message: i18n.translate(
|
||||
'alertsUIShared.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage',
|
||||
{
|
||||
defaultMessage: 'This connector requires a {minimumLicenseRequired} license.',
|
||||
values: {
|
||||
minimumLicenseRequired: upperFirst(actionType.minimumLicenseRequired),
|
||||
},
|
||||
}
|
||||
),
|
||||
messageCard: (
|
||||
<EuiCard
|
||||
titleSize="xs"
|
||||
title={i18n.translate(
|
||||
'alertsUIShared.licenseCheck.actionTypeDisabledByLicenseMessageTitle',
|
||||
{
|
||||
defaultMessage: 'This feature requires a {minimumLicenseRequired} license.',
|
||||
values: {
|
||||
minimumLicenseRequired: upperFirst(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(
|
||||
'alertsUIShared.licenseCheck.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="alertsUIShared.licenseCheck.actionTypeDisabledByLicenseLinkTitle"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const configurationCheckResult = {
|
||||
isEnabled: false,
|
||||
message: i18n.translate(
|
||||
'alertsUIShared.checkActionTypeEnabled.actionTypeDisabledByConfigMessage',
|
||||
{ defaultMessage: 'This connector is disabled by the Kibana configuration.' }
|
||||
),
|
||||
messageCard: (
|
||||
<EuiCard
|
||||
title={i18n.translate('alertsUIShared.licenseCheck.actionTypeDisabledByConfigMessageTitle', {
|
||||
defaultMessage: 'This feature is disabled by the Kibana configuration.',
|
||||
})}
|
||||
description=""
|
||||
className="actCheckActionTypeEnabled__disabledActionWarningCard"
|
||||
/>
|
||||
),
|
||||
};
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { RuleTypeModel, RuleTypeParams, RuleTypeWithDescription } from '../../common';
|
||||
import { getActionGroups, getSelectedActionGroup } from './get_selected_action_group';
|
||||
|
||||
describe('getActionGroups', () => {
|
||||
test('should get action groups when setting recovery context', () => {
|
||||
const actionGroups = getActionGroups({
|
||||
ruleType: {
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
name: 'group-1',
|
||||
},
|
||||
{
|
||||
id: 'group-2',
|
||||
name: 'group-2',
|
||||
},
|
||||
],
|
||||
recoveryActionGroup: {
|
||||
id: 'group-1',
|
||||
},
|
||||
doesSetRecoveryContext: true,
|
||||
} as RuleTypeWithDescription,
|
||||
ruleTypeModel: {
|
||||
defaultRecoveryMessage: 'default recovery message',
|
||||
defaultActionMessage: 'default action message',
|
||||
} as RuleTypeModel<RuleTypeParams>,
|
||||
});
|
||||
|
||||
expect(actionGroups).toEqual([
|
||||
{
|
||||
defaultActionMessage: 'default recovery message',
|
||||
id: 'group-1',
|
||||
name: 'group-1',
|
||||
omitMessageVariables: 'keepContext',
|
||||
},
|
||||
{
|
||||
defaultActionMessage: 'default action message',
|
||||
id: 'group-2',
|
||||
name: 'group-2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should get action groups when not setting recovery context', () => {
|
||||
const actionGroups = getActionGroups({
|
||||
ruleType: {
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
name: 'group-1',
|
||||
},
|
||||
{
|
||||
id: 'group-2',
|
||||
name: 'group-2',
|
||||
},
|
||||
],
|
||||
recoveryActionGroup: {
|
||||
id: 'group-1',
|
||||
},
|
||||
doesSetRecoveryContext: false,
|
||||
} as RuleTypeWithDescription,
|
||||
ruleTypeModel: {
|
||||
defaultRecoveryMessage: 'default recovery message',
|
||||
defaultActionMessage: 'default action message',
|
||||
} as RuleTypeModel<RuleTypeParams>,
|
||||
});
|
||||
|
||||
expect(actionGroups).toEqual([
|
||||
{
|
||||
defaultActionMessage: 'default recovery message',
|
||||
id: 'group-1',
|
||||
name: 'group-1',
|
||||
omitMessageVariables: 'all',
|
||||
},
|
||||
{
|
||||
defaultActionMessage: 'default action message',
|
||||
id: 'group-2',
|
||||
name: 'group-2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSelectedActionGroup', () => {
|
||||
test('should get selected action group', () => {
|
||||
const result = getSelectedActionGroup({
|
||||
group: 'group-1',
|
||||
ruleType: {
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
name: 'group-1',
|
||||
},
|
||||
{
|
||||
id: 'group-2',
|
||||
name: 'group-2',
|
||||
},
|
||||
],
|
||||
recoveryActionGroup: {
|
||||
id: 'group-1',
|
||||
},
|
||||
doesSetRecoveryContext: false,
|
||||
} as RuleTypeWithDescription,
|
||||
ruleTypeModel: {
|
||||
defaultRecoveryMessage: 'default recovery message',
|
||||
defaultActionMessage: 'default action message',
|
||||
} as RuleTypeModel<RuleTypeParams>,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
defaultActionMessage: 'default recovery message',
|
||||
id: 'group-1',
|
||||
name: 'group-1',
|
||||
omitMessageVariables: 'all',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleTypeModel, RuleTypeParams, RuleTypeWithDescription } from '../../common';
|
||||
|
||||
const recoveredActionGroupMessage = i18n.translate(
|
||||
'alertsUIShared.actionForm.actionGroupRecoveredMessage',
|
||||
{
|
||||
defaultMessage: 'Recovered',
|
||||
}
|
||||
);
|
||||
|
||||
export const getActionGroups = ({
|
||||
ruleType,
|
||||
ruleTypeModel,
|
||||
}: {
|
||||
ruleType: RuleTypeWithDescription;
|
||||
ruleTypeModel: RuleTypeModel<RuleTypeParams>;
|
||||
}) => {
|
||||
return ruleType.actionGroups.map((item) =>
|
||||
item.id === ruleType.recoveryActionGroup.id
|
||||
? {
|
||||
...item,
|
||||
omitMessageVariables: ruleType.doesSetRecoveryContext ? 'keepContext' : 'all',
|
||||
defaultActionMessage: ruleTypeModel.defaultRecoveryMessage || recoveredActionGroupMessage,
|
||||
}
|
||||
: { ...item, defaultActionMessage: ruleTypeModel.defaultActionMessage }
|
||||
);
|
||||
};
|
||||
|
||||
export const getSelectedActionGroup = ({
|
||||
group,
|
||||
ruleType,
|
||||
ruleTypeModel,
|
||||
}: {
|
||||
group: string;
|
||||
ruleType: RuleTypeWithDescription;
|
||||
ruleTypeModel: RuleTypeModel<RuleTypeParams>;
|
||||
}) => {
|
||||
const actionGroups = getActionGroups({
|
||||
ruleType,
|
||||
ruleTypeModel,
|
||||
});
|
||||
|
||||
const defaultActionGroup = actionGroups?.find(({ id }) => id === ruleType.defaultActionGroupId);
|
||||
|
||||
return actionGroups?.find(({ id }) => id === group) ?? defaultActionGroup;
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { AlertConsumers, ES_QUERY_ID } from '@kbn/rule-data-utils';
|
||||
import { RuleTypeWithDescription } from '../../common/types';
|
||||
import { hasFieldsForAad } from './has_fields_for_aad';
|
||||
|
||||
describe('hasFieldsForAad', () => {
|
||||
test('should return true if alert has fields for add', () => {
|
||||
const hasFields = hasFieldsForAad({
|
||||
ruleType: {
|
||||
hasFieldsForAAD: true,
|
||||
} as RuleTypeWithDescription,
|
||||
consumer: 'stackAlerts',
|
||||
validConsumers: [],
|
||||
});
|
||||
|
||||
expect(hasFields).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should return true if producer is SIEM', () => {
|
||||
const hasFields = hasFieldsForAad({
|
||||
ruleType: {
|
||||
hasFieldsForAAD: false,
|
||||
producer: AlertConsumers.SIEM,
|
||||
} as RuleTypeWithDescription,
|
||||
consumer: 'stackAlerts',
|
||||
validConsumers: [],
|
||||
});
|
||||
|
||||
expect(hasFields).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should return true if has alerts mappings', () => {
|
||||
const hasFields = hasFieldsForAad({
|
||||
ruleType: {
|
||||
hasFieldsForAAD: false,
|
||||
hasAlertsMappings: true,
|
||||
} as RuleTypeWithDescription,
|
||||
consumer: 'stackAlerts',
|
||||
validConsumers: [],
|
||||
});
|
||||
|
||||
expect(hasFields).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should return true if it is a multi-consumer rule and valid consumer contains it', () => {
|
||||
const hasFields = hasFieldsForAad({
|
||||
ruleType: {
|
||||
hasFieldsForAAD: true,
|
||||
id: ES_QUERY_ID,
|
||||
} as RuleTypeWithDescription,
|
||||
consumer: 'stackAlerts',
|
||||
validConsumers: ['stackAlerts'],
|
||||
});
|
||||
|
||||
expect(hasFields).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should return false if it is a multi-consumer rule and valid consumer does not contain it', () => {
|
||||
const hasFields = hasFieldsForAad({
|
||||
ruleType: {
|
||||
hasFieldsForAAD: true,
|
||||
id: ES_QUERY_ID,
|
||||
} as RuleTypeWithDescription,
|
||||
consumer: 'stackAlerts',
|
||||
validConsumers: ['logs'],
|
||||
});
|
||||
|
||||
expect(hasFields).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { AlertConsumers, RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
import { RuleTypeWithDescription } from '../../common/types';
|
||||
import { DEFAULT_VALID_CONSUMERS, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants';
|
||||
|
||||
export const hasFieldsForAad = ({
|
||||
ruleType,
|
||||
consumer,
|
||||
validConsumers,
|
||||
}: {
|
||||
ruleType: RuleTypeWithDescription;
|
||||
consumer: string;
|
||||
validConsumers: RuleCreationValidConsumer[];
|
||||
}) => {
|
||||
const hasAlertHasData = ruleType
|
||||
? ruleType.hasFieldsForAAD ||
|
||||
ruleType.producer === AlertConsumers.SIEM ||
|
||||
ruleType.hasAlertsMappings
|
||||
: false;
|
||||
|
||||
if (MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleType.id)) {
|
||||
return !!(
|
||||
(validConsumers || DEFAULT_VALID_CONSUMERS).includes(consumer as RuleCreationValidConsumer) &&
|
||||
hasAlertHasData
|
||||
);
|
||||
}
|
||||
return !!hasAlertHasData;
|
||||
};
|
|
@ -14,4 +14,6 @@ export * from './get_authorized_rule_types';
|
|||
export * from './get_authorized_consumers';
|
||||
export * from './get_initial_multi_consumer';
|
||||
export * from './get_initial_schedule';
|
||||
export * from './has_fields_for_aad';
|
||||
export * from './get_selected_action_group';
|
||||
export * from './get_initial_consumer';
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
*/
|
||||
|
||||
export * from './validate_form';
|
||||
export * from './validate_params_for_warnings';
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { validateRuleBase, validateRuleParams, hasRuleErrors } from './validate_form';
|
||||
import {
|
||||
validateRuleBase,
|
||||
validateRuleParams,
|
||||
hasRuleErrors,
|
||||
validateAction,
|
||||
} from './validate_form';
|
||||
import { RuleFormData } from '../types';
|
||||
import {
|
||||
CONSUMER_REQUIRED_TEXT,
|
||||
|
@ -19,6 +24,7 @@ import {
|
|||
} from '../translations';
|
||||
import { formatDuration } from '../utils';
|
||||
import { RuleTypeModel } from '../../common';
|
||||
import { getAction } from '../../common/test_utils/actions_test_utils';
|
||||
|
||||
const formDataMock: RuleFormData = {
|
||||
params: {
|
||||
|
@ -32,6 +38,7 @@ const formDataMock: RuleFormData = {
|
|||
index: ['.kibana'],
|
||||
timeField: 'alert.executionStatus.lastExecutionDate',
|
||||
},
|
||||
actions: [],
|
||||
consumer: 'stackAlerts',
|
||||
schedule: { interval: '1m' },
|
||||
tags: [],
|
||||
|
@ -50,6 +57,23 @@ const ruleTypeModelMock = {
|
|||
}),
|
||||
};
|
||||
|
||||
describe('validateAction', () => {
|
||||
test('should validate filter query', () => {
|
||||
const result = validateAction({
|
||||
action: getAction('1', {
|
||||
alertsFilter: {
|
||||
query: {
|
||||
kql: '',
|
||||
filters: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toEqual({ filterQuery: ['A custom query is required.'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateRuleBase', () => {
|
||||
test('should validate name', () => {
|
||||
const result = validateRuleBase({
|
||||
|
@ -153,6 +177,8 @@ describe('hasRuleErrors', () => {
|
|||
const result = hasRuleErrors({
|
||||
baseErrors: {},
|
||||
paramsErrors: {},
|
||||
actionsErrors: {},
|
||||
actionsParamsErrors: {},
|
||||
});
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
|
@ -164,6 +190,8 @@ describe('hasRuleErrors', () => {
|
|||
name: ['error'],
|
||||
},
|
||||
paramsErrors: {},
|
||||
actionsErrors: {},
|
||||
actionsParamsErrors: {},
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
|
@ -175,6 +203,19 @@ describe('hasRuleErrors', () => {
|
|||
paramsErrors: {
|
||||
someValue: ['error'],
|
||||
},
|
||||
actionsErrors: {},
|
||||
actionsParamsErrors: {},
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
result = hasRuleErrors({
|
||||
baseErrors: {},
|
||||
paramsErrors: {
|
||||
someValue: 'error',
|
||||
},
|
||||
actionsErrors: {},
|
||||
actionsParamsErrors: {},
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
|
@ -186,6 +227,8 @@ describe('hasRuleErrors', () => {
|
|||
someValue: ['error'],
|
||||
},
|
||||
},
|
||||
actionsErrors: {},
|
||||
actionsParamsErrors: {},
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import { isObject } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleFormData } from '../types';
|
||||
import { parseDuration, formatDuration } from '../utils';
|
||||
import {
|
||||
|
@ -20,11 +21,32 @@ import {
|
|||
} from '../translations';
|
||||
import {
|
||||
MinimumScheduleInterval,
|
||||
RuleFormActionsErrors,
|
||||
RuleFormBaseErrors,
|
||||
RuleFormParamsErrors,
|
||||
RuleTypeModel,
|
||||
RuleUiAction,
|
||||
} from '../../common';
|
||||
|
||||
export const validateAction = ({ action }: { action: RuleUiAction }): RuleFormActionsErrors => {
|
||||
const errors = {
|
||||
filterQuery: new Array<string>(),
|
||||
};
|
||||
|
||||
if ('alertsFilter' in action) {
|
||||
const query = action?.alertsFilter?.query;
|
||||
if (query && !query.kql) {
|
||||
errors.filterQuery.push(
|
||||
i18n.translate('alertsUIShared.ruleForm.actionsForm.requiredFilterQuery', {
|
||||
defaultMessage: 'A custom query is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export function validateRuleBase({
|
||||
formData,
|
||||
minimumScheduleInterval,
|
||||
|
@ -93,7 +115,13 @@ const hasRuleBaseErrors = (errors: RuleFormBaseErrors) => {
|
|||
return Object.values(errors).some((error: string[]) => error.length > 0);
|
||||
};
|
||||
|
||||
const hasRuleParamsErrors = (errors: RuleFormParamsErrors): boolean => {
|
||||
const hasActionsError = (actionsErrors: Record<string, RuleFormActionsErrors>) => {
|
||||
return Object.values(actionsErrors).some((errors: RuleFormActionsErrors) => {
|
||||
return Object.values(errors).some((error: string[]) => error.length > 0);
|
||||
});
|
||||
};
|
||||
|
||||
const hasParamsErrors = (errors: RuleFormParamsErrors): boolean => {
|
||||
const values = Object.values(errors);
|
||||
let hasError = false;
|
||||
for (const value of values) {
|
||||
|
@ -104,18 +132,33 @@ const hasRuleParamsErrors = (errors: RuleFormParamsErrors): boolean => {
|
|||
return true;
|
||||
}
|
||||
if (isObject(value)) {
|
||||
hasError = hasRuleParamsErrors(value as RuleFormParamsErrors);
|
||||
hasError = hasParamsErrors(value as RuleFormParamsErrors);
|
||||
}
|
||||
}
|
||||
return hasError;
|
||||
};
|
||||
|
||||
const hasActionsParamsErrors = (actionsParamsErrors: Record<string, RuleFormParamsErrors>) => {
|
||||
return Object.values(actionsParamsErrors).some((errors: RuleFormParamsErrors) => {
|
||||
return hasParamsErrors(errors);
|
||||
});
|
||||
};
|
||||
|
||||
export const hasRuleErrors = ({
|
||||
baseErrors,
|
||||
paramsErrors,
|
||||
actionsErrors,
|
||||
actionsParamsErrors,
|
||||
}: {
|
||||
baseErrors: RuleFormBaseErrors;
|
||||
paramsErrors: RuleFormParamsErrors;
|
||||
actionsErrors: Record<string, RuleFormActionsErrors>;
|
||||
actionsParamsErrors: Record<string, RuleFormParamsErrors>;
|
||||
}): boolean => {
|
||||
return hasRuleBaseErrors(baseErrors) || hasRuleParamsErrors(paramsErrors);
|
||||
return (
|
||||
hasRuleBaseErrors(baseErrors) ||
|
||||
hasParamsErrors(paramsErrors) ||
|
||||
hasActionsError(actionsErrors) ||
|
||||
hasActionsParamsErrors(actionsParamsErrors)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ActionVariable } from '@kbn/alerting-types';
|
||||
import { validateParamsForWarnings } from './validate_params_for_warnings';
|
||||
|
||||
describe('validateParamsForWarnings', () => {
|
||||
const actionVariables: ActionVariable[] = [
|
||||
{
|
||||
name: 'context.url',
|
||||
description: 'Test url',
|
||||
usesPublicBaseUrl: true,
|
||||
},
|
||||
{
|
||||
name: 'context.name',
|
||||
description: 'Test name',
|
||||
},
|
||||
];
|
||||
|
||||
test('returns warnings when publicUrl is not set and there are publicUrl variables used', () => {
|
||||
const warning =
|
||||
'server.publicBaseUrl is not set. Generated URLs will be either relative or empty.';
|
||||
expect(
|
||||
validateParamsForWarnings({
|
||||
value: 'Test for {{context.url}}',
|
||||
actionVariables,
|
||||
})
|
||||
).toEqual(warning);
|
||||
|
||||
expect(
|
||||
validateParamsForWarnings({
|
||||
value: 'link: {{ context.url }}',
|
||||
actionVariables,
|
||||
})
|
||||
).toEqual(warning);
|
||||
|
||||
expect(
|
||||
validateParamsForWarnings({
|
||||
value: '{{=<% %>=}}link: <%context.url%>',
|
||||
actionVariables,
|
||||
})
|
||||
).toEqual(warning);
|
||||
});
|
||||
|
||||
test('does not return warnings when publicUrl is not set and there are publicUrl variables not used', () => {
|
||||
expect(
|
||||
validateParamsForWarnings({
|
||||
value: 'Test for {{context.name}}',
|
||||
actionVariables,
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('does not return warnings when publicUrl is set and there are publicUrl variables used', () => {
|
||||
expect(
|
||||
validateParamsForWarnings({
|
||||
value: 'Test for {{context.url}}',
|
||||
publicBaseUrl: 'http://test',
|
||||
actionVariables,
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('does not returns warnings when publicUrl is not set and the value is not a string', () => {
|
||||
expect(
|
||||
validateParamsForWarnings({
|
||||
value: 10,
|
||||
actionVariables,
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('does not throw an error when passing in invalid mustache', () => {
|
||||
expect(() =>
|
||||
validateParamsForWarnings({
|
||||
value: '{{',
|
||||
actionVariables,
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import Mustache from 'mustache';
|
||||
import { some } from 'lodash';
|
||||
import { ActionVariable, RuleActionParam } from '@kbn/alerting-types';
|
||||
|
||||
const publicUrlWarning = i18n.translate('alertsUIShared.ruleForm.actionsForm.publicBaseUrl', {
|
||||
defaultMessage:
|
||||
'server.publicBaseUrl is not set. Generated URLs will be either relative or empty.',
|
||||
});
|
||||
|
||||
export const validateParamsForWarnings = ({
|
||||
value,
|
||||
publicBaseUrl,
|
||||
actionVariables,
|
||||
}: {
|
||||
value: RuleActionParam;
|
||||
publicBaseUrl?: string;
|
||||
actionVariables?: ActionVariable[];
|
||||
}): string | null => {
|
||||
if (!publicBaseUrl && value && typeof value === 'string') {
|
||||
const publicUrlFields = (actionVariables || []).reduce((acc, v) => {
|
||||
if (v.usesPublicBaseUrl) {
|
||||
acc.push(v.name.replace(/^(params\.|context\.|state\.)/, ''));
|
||||
acc.push(v.name);
|
||||
}
|
||||
return acc;
|
||||
}, new Array<string>());
|
||||
|
||||
try {
|
||||
const variables = new Set(
|
||||
(Mustache.parse(value) as Array<[string, string]>)
|
||||
.filter(([type]) => type === 'name')
|
||||
.map(([, v]) => v)
|
||||
);
|
||||
const hasUrlFields = some(publicUrlFields, (publicUrlField) => variables.has(publicUrlField));
|
||||
if (hasUrlFields) {
|
||||
return publicUrlWarning;
|
||||
}
|
||||
} catch (e) {
|
||||
// Better to set the warning msg if you do not know if the mustache template is invalid
|
||||
return publicUrlWarning;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -49,5 +49,6 @@
|
|||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/core-http-browser-mocks",
|
||||
"@kbn/core-notifications-browser-mocks",
|
||||
"@kbn/kibana-react-plugin"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -38,16 +38,14 @@ import { RuleStatusFilterSandbox } from './components/rule_status_filter_sandbox
|
|||
import { AlertsTableSandbox } from './components/alerts_table_sandbox';
|
||||
import { RulesSettingsLinkSandbox } from './components/rules_settings_link_sandbox';
|
||||
|
||||
import { RuleActionsSandbox } from './components/rule_form/rule_actions_sandbox';
|
||||
import { RuleDetailsSandbox } from './components/rule_form/rule_details_sandbox';
|
||||
|
||||
export interface TriggersActionsUiExampleComponentParams {
|
||||
http: CoreStart['http'];
|
||||
notification: CoreStart['notifications'];
|
||||
notifications: CoreStart['notifications'];
|
||||
application: CoreStart['application'];
|
||||
docLinks: CoreStart['docLinks'];
|
||||
i18n: CoreStart['i18n'];
|
||||
theme: CoreStart['theme'];
|
||||
settings: CoreStart['settings'];
|
||||
history: ScopedHistory;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -62,7 +60,8 @@ const TriggersActionsUiExampleApp = ({
|
|||
triggersActionsUi,
|
||||
http,
|
||||
application,
|
||||
notification,
|
||||
notifications,
|
||||
settings,
|
||||
docLinks,
|
||||
i18n,
|
||||
theme,
|
||||
|
@ -192,7 +191,7 @@ const TriggersActionsUiExampleApp = ({
|
|||
plugins={{
|
||||
http,
|
||||
application,
|
||||
notification,
|
||||
notifications,
|
||||
docLinks,
|
||||
i18n,
|
||||
theme,
|
||||
|
@ -200,7 +199,9 @@ const TriggersActionsUiExampleApp = ({
|
|||
data,
|
||||
dataViews,
|
||||
unifiedSearch,
|
||||
settings,
|
||||
ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry,
|
||||
actionTypeRegistry: triggersActionsUi.actionTypeRegistry,
|
||||
}}
|
||||
returnUrl={application.getUrlForApp('triggersActionsUiExample')}
|
||||
/>
|
||||
|
@ -216,7 +217,7 @@ const TriggersActionsUiExampleApp = ({
|
|||
plugins={{
|
||||
http,
|
||||
application,
|
||||
notification,
|
||||
notifications,
|
||||
docLinks,
|
||||
theme,
|
||||
i18n,
|
||||
|
@ -224,31 +225,15 @@ const TriggersActionsUiExampleApp = ({
|
|||
data,
|
||||
dataViews,
|
||||
unifiedSearch,
|
||||
settings,
|
||||
ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry,
|
||||
actionTypeRegistry: triggersActionsUi.actionTypeRegistry,
|
||||
}}
|
||||
returnUrl={application.getUrlForApp('triggersActionsUiExample')}
|
||||
/>
|
||||
</Page>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/rule_actions"
|
||||
render={() => (
|
||||
<Page title="Rule Actions">
|
||||
<RuleActionsSandbox />
|
||||
</Page>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/rule_details"
|
||||
render={() => (
|
||||
<Page title="Rule Details">
|
||||
<RuleDetailsSandbox />
|
||||
</Page>
|
||||
)}
|
||||
/>
|
||||
</Routes>
|
||||
</EuiPage>
|
||||
</Router>
|
||||
|
@ -262,7 +247,7 @@ export const renderApp = (
|
|||
deps: TriggersActionsUiExamplePublicStartDeps,
|
||||
{ appBasePath, element, history }: AppMountParameters
|
||||
) => {
|
||||
const { http, notifications, docLinks, application, i18n, theme } = core;
|
||||
const { http, notifications, docLinks, application, i18n, theme, settings } = core;
|
||||
const { triggersActionsUi } = deps;
|
||||
const { ruleTypeRegistry, actionTypeRegistry } = triggersActionsUi;
|
||||
|
||||
|
@ -281,11 +266,12 @@ export const renderApp = (
|
|||
<TriggersActionsUiExampleApp
|
||||
history={history}
|
||||
http={http}
|
||||
notification={notifications}
|
||||
notifications={notifications}
|
||||
application={application}
|
||||
docLinks={docLinks}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
settings={settings}
|
||||
triggersActionsUi={deps.triggersActionsUi}
|
||||
data={deps.data}
|
||||
charts={deps.charts}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { RuleActions } from '@kbn/alerts-ui-shared/src/rule_form';
|
||||
|
||||
export const RuleActionsSandbox = () => {
|
||||
return <RuleActions onClick={() => {}} />;
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { RuleDetails } from '@kbn/alerts-ui-shared/src/rule_form';
|
||||
|
||||
export const RuleDetailsSandbox = () => {
|
||||
return <RuleDetails />;
|
||||
};
|
|
@ -89,16 +89,6 @@ export const Sidebar = ({ history }: { history: ScopedHistory }) => {
|
|||
name: 'Rule Edit',
|
||||
onClick: () => history.push('/rule/edit/test'),
|
||||
},
|
||||
{
|
||||
id: 'rule-actions',
|
||||
name: 'Rule Actions',
|
||||
onClick: () => history.push('/rule_actions'),
|
||||
},
|
||||
{
|
||||
id: 'rule-details',
|
||||
name: 'Rule Details',
|
||||
onClick: () => history.push('/rule_details'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -45573,8 +45573,6 @@
|
|||
"xpack.triggersActionsUI.bulkActions.selectRowCheckbox.AriaLabel": "Sélectionner la ligne {displayedRowIndex}",
|
||||
"xpack.triggersActionsUI.cases.api.bulkGet": "Erreur lors de la récupération des données sur les cas",
|
||||
"xpack.triggersActionsUI.cases.label": "Cas",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "Ce connecteur est désactivé par la configuration de Kibana.",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "Ce connecteur requiert une licence {minimumLicenseRequired}.",
|
||||
"xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "Ce type de règle requiert une licence {minimumLicenseRequired}.",
|
||||
"xpack.triggersActionsUI.common.constants.comparators.groupByTypes.allDocumentsLabel": "tous les documents",
|
||||
"xpack.triggersActionsUI.common.constants.comparators.groupByTypes.topLabel": "premiers",
|
||||
|
@ -45704,10 +45702,6 @@
|
|||
"xpack.triggersActionsUI.inspect.modal.somethingWentWrongDescription": "Désolé, un problème est survenu.",
|
||||
"xpack.triggersActionsUI.inspectDescription": "Inspecter",
|
||||
"xpack.triggersActionsUI.jsonFieldWrapper.defaultLabel": "Éditeur JSON",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByConfigMessageTitle": "Cette fonctionnalité est désactivée par la configuration de Kibana.",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseLinkTitle": "Afficher les options de licence",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseMessageDescription": "Pour réactiver cette action, veuillez mettre à niveau votre licence.",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseMessageTitle": "Cette fonctionnalité requiert une licence {minimumLicenseRequired}.",
|
||||
"xpack.triggersActionsUI.logs.breadcrumbTitle": "Logs",
|
||||
"xpack.triggersActionsUI.maintenanceWindows.label": "Fenêtres de maintenance",
|
||||
"xpack.triggersActionsUI.managementSection.alerts.displayDescription": "Monitorer toutes vos alertes au même endroit",
|
||||
|
@ -45851,8 +45845,6 @@
|
|||
"xpack.triggersActionsUI.sections.actionsConnectorsList.unableToLoadConnectorTypesMessage": "Impossible de charger les types de connecteurs",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.warningText": "{connectors, plural, one {Ce connecteur est} other {Certains connecteurs sont}} actuellement en cours d'utilisation.",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.accordion.deleteIconAriaLabel": "Supprimer",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryPlaceholder": "Filtrer les alertes à l'aide de la syntaxe KQL",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryToggleLabel": "Si l'alerte correspond à une requête",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionDisabledTitle": "Cette action est désactivée",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionErrorToolTip": "L’action contient des erreurs.",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionIdLabel": "Connecteur {connectorInstance}",
|
||||
|
|
|
@ -45315,8 +45315,6 @@
|
|||
"xpack.triggersActionsUI.bulkActions.selectRowCheckbox.AriaLabel": "行\"{displayedRowIndex}\"を選択",
|
||||
"xpack.triggersActionsUI.cases.api.bulkGet": "ケースデータの取得エラー",
|
||||
"xpack.triggersActionsUI.cases.label": "ケース",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "このコネクターは Kibana の構成で無効になっています。",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "このコネクターには {minimumLicenseRequired} ライセンスが必要です。",
|
||||
"xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "このルールタイプには{minimumLicenseRequired}ライセンスが必要です。",
|
||||
"xpack.triggersActionsUI.common.constants.comparators.groupByTypes.allDocumentsLabel": "すべてのドキュメント",
|
||||
"xpack.triggersActionsUI.common.constants.comparators.groupByTypes.topLabel": "トップ",
|
||||
|
@ -45445,10 +45443,6 @@
|
|||
"xpack.triggersActionsUI.inspect.modal.somethingWentWrongDescription": "申し訳ございませんが、何か問題が発生しました。",
|
||||
"xpack.triggersActionsUI.inspectDescription": "検査",
|
||||
"xpack.triggersActionsUI.jsonFieldWrapper.defaultLabel": "JSONエディター",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByConfigMessageTitle": "この機能は Kibana の構成で無効になっています。",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseLinkTitle": "ライセンスオプションを表示",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseMessageDescription": "このアクションを再び有効にするには、ライセンスをアップグレードしてください。",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseMessageTitle": "この機能には {minimumLicenseRequired} ライセンスが必要です。",
|
||||
"xpack.triggersActionsUI.logs.breadcrumbTitle": "ログ",
|
||||
"xpack.triggersActionsUI.maintenanceWindows.label": "保守時間枠",
|
||||
"xpack.triggersActionsUI.managementSection.alerts.displayDescription": "すべてのアラートを1か所で監視",
|
||||
|
@ -45592,8 +45586,6 @@
|
|||
"xpack.triggersActionsUI.sections.actionsConnectorsList.unableToLoadConnectorTypesMessage": "コネクタータイプを読み込めません",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.warningText": "{connectors, plural, one {このコネクターは} other {一部のコネクターが}}現在使用中です。",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.accordion.deleteIconAriaLabel": "削除",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryPlaceholder": "KQL構文を使用してアラートをフィルター",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryToggleLabel": "アラートがクエリと一致する場合",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionDisabledTitle": "このアクションは無効です",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionErrorToolTip": "アクションにはエラーがあります。",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionIdLabel": "{connectorInstance}コネクター",
|
||||
|
|
|
@ -45368,8 +45368,6 @@
|
|||
"xpack.triggersActionsUI.bulkActions.selectRowCheckbox.AriaLabel": "选择行 {displayedRowIndex}",
|
||||
"xpack.triggersActionsUI.cases.api.bulkGet": "提取案例数据时出错",
|
||||
"xpack.triggersActionsUI.cases.label": "案例",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "连接器已由 Kibana 配置禁用。",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "此连接器需要{minimumLicenseRequired}许可证。",
|
||||
"xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "此规则类型需要{minimumLicenseRequired}许可证。",
|
||||
"xpack.triggersActionsUI.common.constants.comparators.groupByTypes.allDocumentsLabel": "所有文档",
|
||||
"xpack.triggersActionsUI.common.constants.comparators.groupByTypes.topLabel": "排名前",
|
||||
|
@ -45498,10 +45496,6 @@
|
|||
"xpack.triggersActionsUI.inspect.modal.somethingWentWrongDescription": "抱歉,出现问题。",
|
||||
"xpack.triggersActionsUI.inspectDescription": "检查",
|
||||
"xpack.triggersActionsUI.jsonFieldWrapper.defaultLabel": "JSON 编辑器",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByConfigMessageTitle": "此功能已由 Kibana 配置禁用。",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseLinkTitle": "查看许可证选项",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseMessageDescription": "要重新启用此操作,请升级您的许可证。",
|
||||
"xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseMessageTitle": "此功能需要{minimumLicenseRequired}许可证。",
|
||||
"xpack.triggersActionsUI.logs.breadcrumbTitle": "日志",
|
||||
"xpack.triggersActionsUI.maintenanceWindows.label": "维护窗口",
|
||||
"xpack.triggersActionsUI.managementSection.alerts.displayDescription": "在一个位置监测您的所有告警",
|
||||
|
@ -45645,8 +45639,6 @@
|
|||
"xpack.triggersActionsUI.sections.actionsConnectorsList.unableToLoadConnectorTypesMessage": "无法加载连接器类型",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.warningText": "{connectors, plural, one {此连接器} other {这些连接器}}当前正在使用中。",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.accordion.deleteIconAriaLabel": "删除",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryPlaceholder": "使用 KQL 语法筛选告警",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterQueryToggleLabel": "如果告警与查询匹配",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionDisabledTitle": "此操作已禁用",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionErrorToolTip": "操作包含错误。",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.actionIdLabel": "{connectorInstance} 连接器",
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiCard, EuiLink } from '@elastic/eui';
|
||||
import { ActionType, ActionConnector } from '../../types';
|
||||
import { VIEW_LICENSE_OPTIONS_LINK } from '../../common/constants';
|
||||
import './check_action_type_enabled.scss';
|
||||
|
||||
export interface IsEnabledResult {
|
||||
isEnabled: true;
|
||||
}
|
||||
export interface IsDisabledResult {
|
||||
isEnabled: false;
|
||||
message: string;
|
||||
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: upperFirst(actionType.minimumLicenseRequired),
|
||||
},
|
||||
}
|
||||
),
|
||||
messageCard: (
|
||||
<EuiCard
|
||||
titleSize="xs"
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseMessageTitle',
|
||||
{
|
||||
defaultMessage: 'This feature requires a {minimumLicenseRequired} license.',
|
||||
values: {
|
||||
minimumLicenseRequired: upperFirst(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.licenseCheck.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.licenseCheck.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.licenseCheck.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 getLicenseCheckResult(actionType);
|
||||
}
|
||||
|
||||
if (actionType?.enabledInConfig === false) {
|
||||
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 };
|
||||
}
|
|
@ -29,6 +29,7 @@ import {
|
|||
} from '@kbn/alerting-plugin/common';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ActionGroupWithMessageVariables } from '@kbn/triggers-actions-ui-types';
|
||||
import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared/src/rule_form/utils/check_action_type_enabled';
|
||||
import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL } from '../translations';
|
||||
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
|
||||
import {
|
||||
|
@ -45,7 +46,6 @@ import { SectionLoading } from '../../components/section_loading';
|
|||
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 { DEFAULT_FREQUENCY, VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { ConnectorAddModal } from '.';
|
||||
|
@ -552,7 +552,6 @@ export const ActionForm = ({
|
|||
hasAlertsMappings={hasAlertsMappings}
|
||||
minimumThrottleInterval={minimumThrottleInterval}
|
||||
notifyWhenSelectOptions={notifyWhenSelectOptions}
|
||||
defaultNotifyWhenValue={defaultRuleFrequency.notifyWhen}
|
||||
featureId={featureId}
|
||||
producerId={producerId}
|
||||
ruleTypeId={ruleTypeId}
|
||||
|
|
|
@ -21,11 +21,7 @@ import { EuiFieldText } from '@elastic/eui';
|
|||
import { I18nProvider, __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { render, waitFor, screen } from '@testing-library/react';
|
||||
import { DEFAULT_FREQUENCY } from '../../../common/constants';
|
||||
import {
|
||||
RuleNotifyWhen,
|
||||
RuleNotifyWhenType,
|
||||
SanitizedRuleAction,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { RuleNotifyWhen, SanitizedRuleAction } from '@kbn/alerting-plugin/common';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { transformActionVariables } from '@kbn/alerts-ui-shared/src/action_variables/transforms';
|
||||
|
||||
|
@ -557,7 +553,6 @@ describe('action_type_form', () => {
|
|||
index: 1,
|
||||
actionItem,
|
||||
notifyWhenSelectOptions: CUSTOM_NOTIFY_WHEN_OPTIONS,
|
||||
defaultNotifyWhenValue: RuleNotifyWhen.ACTIVE,
|
||||
})}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
@ -611,7 +606,6 @@ describe('action_type_form', () => {
|
|||
index: 1,
|
||||
actionItem,
|
||||
notifyWhenSelectOptions: CUSTOM_NOTIFY_WHEN_OPTIONS,
|
||||
defaultNotifyWhenValue: RuleNotifyWhen.ACTIVE,
|
||||
})}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
@ -650,7 +644,6 @@ function getActionTypeForm({
|
|||
messageVariables = { context: [], state: [], params: [] },
|
||||
summaryMessageVariables = { context: [], state: [], params: [] },
|
||||
notifyWhenSelectOptions,
|
||||
defaultNotifyWhenValue,
|
||||
ruleTypeId,
|
||||
producerId = AlertConsumers.INFRASTRUCTURE,
|
||||
featureId = AlertConsumers.INFRASTRUCTURE,
|
||||
|
@ -671,7 +664,6 @@ function getActionTypeForm({
|
|||
messageVariables?: ActionVariables;
|
||||
summaryMessageVariables?: ActionVariables;
|
||||
notifyWhenSelectOptions?: NotifyWhenSelectOptions[];
|
||||
defaultNotifyWhenValue?: RuleNotifyWhenType;
|
||||
ruleTypeId?: string;
|
||||
producerId?: string;
|
||||
featureId?: string;
|
||||
|
@ -766,7 +758,6 @@ function getActionTypeForm({
|
|||
messageVariables={messageVariables}
|
||||
summaryMessageVariables={summaryMessageVariables}
|
||||
notifyWhenSelectOptions={notifyWhenSelectOptions}
|
||||
defaultNotifyWhenValue={defaultNotifyWhenValue}
|
||||
producerId={producerId}
|
||||
featureId={featureId}
|
||||
ruleTypeId={ruleTypeId}
|
||||
|
|
|
@ -35,8 +35,8 @@ import { isEmpty, partition, some } from 'lodash';
|
|||
import {
|
||||
ActionVariable,
|
||||
RuleActionAlertsFilterProperty,
|
||||
RuleActionFrequency,
|
||||
RuleActionParam,
|
||||
RuleNotifyWhenType,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
getDurationNumberInItsUnit,
|
||||
|
@ -46,6 +46,8 @@ import {
|
|||
import type { SavedObjectAttribute } from '@kbn/core-saved-objects-api-server';
|
||||
import { transformActionVariables } from '@kbn/alerts-ui-shared/src/action_variables/transforms';
|
||||
import { RuleActionsNotifyWhen } from '@kbn/alerts-ui-shared/src/rule_form/rule_actions/rule_actions_notify_when';
|
||||
import { RuleActionsAlertsFilter } from '@kbn/alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter';
|
||||
import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared/src/rule_form/utils/check_action_type_enabled';
|
||||
import { RuleActionsAlertsFilterTimeframe } from '@kbn/alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter_timeframe';
|
||||
import { ActionGroupWithMessageVariables } from '@kbn/triggers-actions-ui-types';
|
||||
import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL } from '../translations';
|
||||
|
@ -60,13 +62,11 @@ import {
|
|||
ActionConnectorMode,
|
||||
NotifyWhenSelectOptions,
|
||||
} from '../../../types';
|
||||
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
|
||||
import { hasSaveActionsCapability } from '../../lib/capabilities';
|
||||
import { ActionAccordionFormProps } from './action_form';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { ConnectorsSelection } from './connectors_selection';
|
||||
import { validateParamsForWarnings } from '../../lib/validate_params_for_warnings';
|
||||
import { ActionAlertsFilterQuery } from './action_alerts_filter_query';
|
||||
import { validateActionFilterQuery } from '../../lib/value_validators';
|
||||
import { useRuleTypeAadTemplateFields } from '../../hooks/use_rule_aad_template_fields';
|
||||
|
||||
|
@ -94,7 +94,6 @@ export type ActionTypeFormProps = {
|
|||
hasAlertsMappings?: boolean;
|
||||
minimumThrottleInterval?: [number | undefined, string];
|
||||
notifyWhenSelectOptions?: NotifyWhenSelectOptions[];
|
||||
defaultNotifyWhenValue?: RuleNotifyWhenType;
|
||||
featureId: string;
|
||||
producerId: string;
|
||||
ruleTypeId?: string;
|
||||
|
@ -146,7 +145,6 @@ export const ActionTypeForm = ({
|
|||
hasAlertsMappings,
|
||||
minimumThrottleInterval,
|
||||
notifyWhenSelectOptions,
|
||||
defaultNotifyWhenValue,
|
||||
producerId,
|
||||
featureId,
|
||||
ruleTypeId,
|
||||
|
@ -157,6 +155,9 @@ export const ActionTypeForm = ({
|
|||
application: { capabilities },
|
||||
settings,
|
||||
http,
|
||||
notifications,
|
||||
unifiedSearch,
|
||||
dataViews,
|
||||
} = useKibana().services;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
@ -369,44 +370,32 @@ export const ActionTypeForm = ({
|
|||
? isActionGroupDisabledForActionType(actionGroupId, actionTypeId)
|
||||
: false;
|
||||
|
||||
const onActionFrequencyChange = (frequency: RuleActionFrequency | undefined) => {
|
||||
const { notifyWhen, throttle, summary } = frequency || {};
|
||||
|
||||
setActionFrequencyProperty('notifyWhen', notifyWhen, index);
|
||||
|
||||
if (throttle) {
|
||||
setActionThrottle(getDurationNumberInItsUnit(throttle));
|
||||
setActionThrottleUnit(getDurationUnitValue(throttle));
|
||||
}
|
||||
|
||||
setActionFrequencyProperty('throttle', throttle ? throttle : null, index);
|
||||
|
||||
setActionFrequencyProperty('summary', summary, index);
|
||||
};
|
||||
|
||||
const actionNotifyWhen = (
|
||||
<RuleActionsNotifyWhen
|
||||
frequency={actionItem.frequency}
|
||||
throttle={actionThrottle}
|
||||
throttleUnit={actionThrottleUnit}
|
||||
hasAlertsMappings={hasAlertsMappings}
|
||||
onNotifyWhenChange={useCallback(
|
||||
(notifyWhen: RuleNotifyWhenType) => {
|
||||
setActionFrequencyProperty('notifyWhen', notifyWhen, index);
|
||||
},
|
||||
[setActionFrequencyProperty, index]
|
||||
)}
|
||||
onThrottleChange={useCallback(
|
||||
(throttle: number | null, throttleUnit: string) => {
|
||||
if (throttle) {
|
||||
setActionThrottle(throttle);
|
||||
setActionThrottleUnit(throttleUnit);
|
||||
}
|
||||
setActionFrequencyProperty(
|
||||
'throttle',
|
||||
throttle ? `${throttle}${throttleUnit}` : null,
|
||||
index
|
||||
);
|
||||
},
|
||||
[setActionFrequencyProperty, index]
|
||||
)}
|
||||
onSummaryChange={useCallback(
|
||||
(summary: boolean) => {
|
||||
// use the default message when a user toggles between action frequencies
|
||||
setUseDefaultMessage(true);
|
||||
setActionFrequencyProperty('summary', summary, index);
|
||||
},
|
||||
[setActionFrequencyProperty, index]
|
||||
)}
|
||||
onChange={onActionFrequencyChange}
|
||||
showMinimumThrottleWarning={showMinimumThrottleWarning}
|
||||
showMinimumThrottleUnitWarning={showMinimumThrottleUnitWarning}
|
||||
notifyWhenSelectOptions={notifyWhenSelectOptions}
|
||||
defaultNotifyWhenValue={defaultNotifyWhenValue}
|
||||
onUseDefaultMessage={() => setUseDefaultMessage(true)}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -515,17 +504,23 @@ export const ActionTypeForm = ({
|
|||
<>
|
||||
{!hideNotifyWhen && <EuiSpacer size="xl" />}
|
||||
<EuiFormRow error={queryError} isInvalid={!!queryError} fullWidth>
|
||||
<ActionAlertsFilterQuery
|
||||
state={actionItem.alertsFilter?.query}
|
||||
<RuleActionsAlertsFilter
|
||||
action={actionItem}
|
||||
onChange={(query) => setActionAlertsFilterProperty('query', query, index)}
|
||||
featureIds={[producerId as ValidFeatureId]}
|
||||
appName={featureId!}
|
||||
ruleTypeId={ruleTypeId}
|
||||
plugins={{
|
||||
http,
|
||||
unifiedSearch,
|
||||
dataViews,
|
||||
notifications,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
<RuleActionsAlertsFilterTimeframe
|
||||
state={actionItem.alertsFilter?.timeframe}
|
||||
action={actionItem}
|
||||
settings={settings}
|
||||
onChange={(timeframe) => setActionAlertsFilterProperty('timeframe', timeframe, index)}
|
||||
/>
|
||||
|
|
|
@ -10,11 +10,11 @@ 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 { checkActionTypeEnabled } from '@kbn/alerts-ui-shared/src/rule_form/utils/check_action_type_enabled';
|
||||
import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL } from '../translations';
|
||||
import { ActionType, ActionTypeIndex, ActionTypeRegistryContract } from '../../../types';
|
||||
import { loadActionTypes } from '../../lib/action_connector_api';
|
||||
import { actionTypeCompare } from '../../lib/action_type_compare';
|
||||
import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { SectionLoading } from '../../components/section_loading';
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import { getConnectorCompatibility } from '@kbn/actions-plugin/common';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { checkActionTypeEnabled } from '@kbn/alerts-ui-shared/src/rule_form/utils/check_action_type_enabled';
|
||||
import { loadActionTypes, deleteActions } from '../../../lib/action_connector_api';
|
||||
import {
|
||||
hasDeleteActionsCapability,
|
||||
|
@ -33,7 +34,6 @@ import {
|
|||
hasExecuteActionsCapability,
|
||||
} from '../../../lib/capabilities';
|
||||
import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation';
|
||||
import { checkActionTypeEnabled } from '../../../lib/check_action_type_enabled';
|
||||
import './actions_connectors_list.scss';
|
||||
import {
|
||||
ActionConnector,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { Comparator, COMPARATORS } from '@kbn/alerting-comparators';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export { VIEW_LICENSE_OPTIONS_LINK } from '@kbn/alerts-ui-shared/src/common/constants';
|
||||
export { AGGREGATION_TYPES, builtInAggregationTypes } from './aggregation_types';
|
||||
export { loadAllActions, loadActionTypes } from '../../application/lib/action_connector_api';
|
||||
export { ConnectorAddModal } from '../../application/sections/action_connector_form';
|
||||
|
@ -16,8 +17,6 @@ export type { ActionConnector } from '../..';
|
|||
export { builtInGroupByTypes } from './group_by_types';
|
||||
export * from './action_frequency_types';
|
||||
|
||||
export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions';
|
||||
|
||||
export const PLUGIN_ID = 'triggersActions';
|
||||
export const ALERTS_PAGE_ID = 'triggersActionsAlerts';
|
||||
export const CONNECTORS_PLUGIN_ID = 'triggersActionsConnectors';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue