[RAM] Add toggle for AAD fields in alert templating (#170162)

## Summary

Reopen of https://github.com/elastic/kibana/pull/161213

Closes https://github.com/elastic/kibana/issues/160838

Feature can be activated via feature flag:
`xpack.trigger_actions_ui.enableExperimental:
['isMustacheAutocompleteOn', 'showMustacheAutocompleteSwitch']`
<img width="605" alt="Screenshot 2023-10-30 at 5 52 39 PM"
src="da24b419-3b08-4014-be2f-99692773755f">
<img width="583" alt="Screenshot 2023-10-30 at 5 52 22 PM"
src="fc5b8a1e-8202-4491-b4fb-694b70809f4d">



### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Julian Gernun <17549662+jcger@users.noreply.github.com>
Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Zacqary Adam Xeper 2023-11-14 12:39:30 -06:00 committed by GitHub
parent a46923cae3
commit 603a0454a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 537 additions and 183 deletions

View file

@ -63,6 +63,7 @@ export const actionSchema = schema.object({
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
frequency: schema.maybe(actionFrequencySchema),
alerts_filter: schema.maybe(actionAlertsFilterSchema),
use_alert_data_for_template: schema.maybe(schema.boolean()),
});
export const createBodySchema = schema.object({

View file

@ -116,6 +116,7 @@ export interface RuleAction {
params: RuleActionParams;
frequency?: RuleActionFrequency;
alertsFilter?: AlertsFilter;
useAlertDataForTemplate?: boolean;
}
export interface RuleLastRun {

View file

@ -6,6 +6,7 @@
*/
import { v4 as uuidV4 } from 'uuid';
import { AADAlert } from '@kbn/alerts-as-data-utils';
import { get, isEmpty } from 'lodash';
import { MutableAlertInstanceMeta } from '@kbn/alerting-state-types';
import { ALERT_UUID } from '@kbn/rule-data-utils';
@ -36,7 +37,7 @@ export type PublicAlert<
Context extends AlertInstanceContext = AlertInstanceContext,
ActionGroupIds extends string = DefaultActionGroupId
> = Pick<
Alert<State, Context, ActionGroupIds>,
Alert<State, Context, ActionGroupIds, AADAlert>,
| 'getContext'
| 'getState'
| 'getUuid'
@ -50,13 +51,15 @@ export type PublicAlert<
export class Alert<
State extends AlertInstanceState = AlertInstanceState,
Context extends AlertInstanceContext = AlertInstanceContext,
ActionGroupIds extends string = never
ActionGroupIds extends string = never,
AlertAsData extends AADAlert = AADAlert
> {
private scheduledExecutionOptions?: ScheduledExecutionOptions<State, Context, ActionGroupIds>;
private meta: MutableAlertInstanceMeta;
private state: State;
private context: Context;
private readonly id: string;
private alertAsData: AlertAsData | undefined;
constructor(id: string, { state, meta = {} }: RawAlertInstance = {}) {
this.id = id;
@ -78,6 +81,18 @@ export class Alert<
return this.meta.uuid!;
}
isAlertAsData() {
return this.alertAsData !== undefined;
}
setAlertAsData(alertAsData: AlertAsData) {
this.alertAsData = alertAsData;
}
getAlertAsData() {
return this.alertAsData;
}
getStart(): string | null {
return this.state.start ? `${this.state.start}` : null;
}

View file

@ -35,6 +35,7 @@ export const createRuleDataSchema = schema.object({
),
uuid: schema.maybe(schema.string()),
alertsFilter: schema.maybe(actionAlertsFilterSchema),
useAlertDataForTemplate: schema.maybe(schema.boolean()),
}),
{ defaultValue: [] }
),

View file

@ -65,6 +65,7 @@ export const actionDomainSchema = schema.object({
params: actionParamsSchema,
frequency: schema.maybe(actionFrequencySchema),
alertsFilter: schema.maybe(actionDomainAlertsFilterSchema),
useAlertDataAsTemplate: schema.maybe(schema.boolean()),
});
/**
@ -89,4 +90,5 @@ export const actionSchema = schema.object({
params: actionParamsSchema,
frequency: schema.maybe(actionFrequencySchema),
alertsFilter: schema.maybe(actionAlertsFilterSchema),
useAlertDataForTemplate: schema.maybe(schema.boolean()),
});

View file

@ -68,6 +68,7 @@ export const actionsSchema = schema.arrayOf(
),
})
),
use_alert_data_for_template: schema.maybe(schema.boolean()),
}),
{ defaultValue: [] }
);

View file

@ -15,20 +15,28 @@ export const rewriteActionsReq = (
): NormalizedAlertAction[] => {
if (!actions) return [];
return actions.map(({ frequency, alerts_filter: alertsFilter, ...action }) => {
return {
...action,
...(frequency
? {
frequency: {
...omit(frequency, 'notify_when'),
notifyWhen: frequency.notify_when,
},
}
: {}),
...(alertsFilter ? { alertsFilter } : {}),
};
});
return actions.map(
({
frequency,
alerts_filter: alertsFilter,
use_alert_data_for_template: useAlertDataForTemplate,
...action
}) => {
return {
...action,
useAlertDataForTemplate,
...(frequency
? {
frequency: {
...omit(frequency, 'notify_when'),
notifyWhen: frequency.notify_when,
},
}
: {}),
...(alertsFilter ? { alertsFilter } : {}),
};
}
);
};
export const rewriteActionsRes = (actions?: RuleAction[]) => {
@ -37,14 +45,17 @@ export const rewriteActionsRes = (actions?: RuleAction[]) => {
notify_when: notifyWhen,
});
if (!actions) return [];
return actions.map(({ actionTypeId, frequency, alertsFilter, ...action }) => ({
...action,
connector_type_id: actionTypeId,
...(frequency ? { frequency: rewriteFrequency(frequency) } : {}),
...(alertsFilter
? {
alerts_filter: alertsFilter,
}
: {}),
}));
return actions.map(
({ actionTypeId, frequency, alertsFilter, useAlertDataForTemplate, ...action }) => ({
...action,
connector_type_id: actionTypeId,
use_alert_data_for_template: useAlertDataForTemplate,
...(frequency ? { frequency: rewriteFrequency(frequency) } : {}),
...(alertsFilter
? {
alerts_filter: alertsFilter,
}
: {}),
})
);
};

View file

@ -114,6 +114,7 @@ describe('bulkEditRulesRoute', () => {
foo: true,
},
uuid: '123-456',
use_alert_data_for_template: false,
},
],
}),

View file

@ -124,6 +124,7 @@ describe('createRuleRoute', () => {
},
connector_type_id: 'test',
uuid: '123-456',
use_alert_data_for_template: false,
},
],
};
@ -198,6 +199,7 @@ describe('createRuleRoute', () => {
"params": Object {
"foo": true,
},
"useAlertDataForTemplate": undefined,
},
],
"alertTypeId": "1",
@ -314,6 +316,7 @@ describe('createRuleRoute', () => {
"params": Object {
"foo": true,
},
"useAlertDataForTemplate": undefined,
},
],
"alertTypeId": "1",
@ -431,6 +434,7 @@ describe('createRuleRoute', () => {
"params": Object {
"foo": true,
},
"useAlertDataForTemplate": undefined,
},
],
"alertTypeId": "1",
@ -548,6 +552,7 @@ describe('createRuleRoute', () => {
"params": Object {
"foo": true,
},
"useAlertDataForTemplate": undefined,
},
],
"alertTypeId": "1",

View file

@ -15,25 +15,33 @@ import type { RuleParams } from '../../../../../../application/rule/types';
const transformCreateBodyActions = (actions: CreateRuleActionV1[]): CreateRuleData['actions'] => {
if (!actions) return [];
return actions.map(({ frequency, alerts_filter: alertsFilter, ...action }) => {
return {
group: action.group,
id: action.id,
params: action.params,
actionTypeId: action.actionTypeId,
...(action.uuid ? { uuid: action.uuid } : {}),
...(frequency
? {
frequency: {
summary: frequency.summary,
throttle: frequency.throttle,
notifyWhen: frequency.notify_when,
},
}
: {}),
...(alertsFilter ? { alertsFilter } : {}),
};
});
return actions.map(
({
frequency,
alerts_filter: alertsFilter,
use_alert_data_for_template: useAlertDataForTemplate,
...action
}) => {
return {
group: action.group,
id: action.id,
params: action.params,
actionTypeId: action.actionTypeId,
useAlertDataForTemplate,
...(action.uuid ? { uuid: action.uuid } : {}),
...(frequency
? {
frequency: {
summary: frequency.summary,
throttle: frequency.throttle,
notifyWhen: frequency.notify_when,
},
}
: {}),
...(alertsFilter ? { alertsFilter } : {}),
};
}
);
};
export const transformCreateBody = <Params extends RuleParams = never>(

View file

@ -45,6 +45,7 @@ describe('resolveRuleRoute', () => {
foo: true,
},
uuid: '123-456',
useAlertDataForTemplate: false,
},
],
consumer: 'bar',
@ -101,6 +102,7 @@ describe('resolveRuleRoute', () => {
params: mockedRule.actions[0].params,
connector_type_id: mockedRule.actions[0].actionTypeId,
uuid: mockedRule.actions[0].uuid,
use_alert_data_for_template: mockedRule.actions[0].useAlertDataForTemplate,
},
],
outcome: 'aliasMatch',

View file

@ -49,11 +49,21 @@ export const transformRuleToRuleResponse = <Params extends RuleParams = never>(
consumer: rule.consumer,
schedule: rule.schedule,
actions: rule.actions.map(
({ group, id, actionTypeId, params, frequency, uuid, alertsFilter }) => ({
({
group,
id,
actionTypeId,
params,
frequency,
uuid,
alertsFilter,
useAlertDataForTemplate,
}) => ({
group,
id,
params,
connector_type_id: actionTypeId,
use_alert_data_for_template: useAlertDataForTemplate ?? false,
...(frequency
? {
frequency: {

View file

@ -132,6 +132,7 @@ describe('updateRuleRoute', () => {
"params": Object {
"baz": true,
},
"useAlertDataForTemplate": undefined,
"uuid": "1234-5678",
},
],

View file

@ -20,12 +20,16 @@ import { ActionsClient } from '@kbn/actions-plugin/server/actions_client';
import { chunk } from 'lodash';
import { GetSummarizedAlertsParams, IAlertsClient } from '../alerts_client/types';
import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event_logger';
import { parseDuration, CombinedSummarizedAlerts, ThrottledActions } from '../types';
import { AlertHit, parseDuration, CombinedSummarizedAlerts, ThrottledActions } from '../types';
import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store';
import { injectActionParams } from './inject_action_params';
import { Executable, ExecutionHandlerOptions, RuleTaskInstance } from './types';
import { TaskRunnerContext } from './task_runner_factory';
import { transformActionParams, transformSummaryActionParams } from './transform_action_params';
import {
transformActionParams,
TransformActionParamsOptions,
transformSummaryActionParams,
} from './transform_action_params';
import { Alert } from '../alert';
import { NormalizedRuleType } from '../rule_type_registry';
import {
@ -292,33 +296,40 @@ export class ExecutionHandler<
};
} else {
const ruleUrl = this.buildRuleUrl(spaceId);
const executableAlert = alert!;
const transformActionParamsOptions: TransformActionParamsOptions = {
actionsPlugin,
alertId: ruleId,
alertType: this.ruleType.id,
actionTypeId,
alertName: this.rule.name,
spaceId,
tags: this.rule.tags,
alertInstanceId: executableAlert.getId(),
alertUuid: executableAlert.getUuid(),
alertActionGroup: actionGroup,
alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!,
context: executableAlert.getContext(),
actionId: action.id,
state: executableAlert.getState(),
kibanaBaseUrl: this.taskRunnerContext.kibanaBaseUrl,
alertParams: this.rule.params,
actionParams: action.params,
flapping: executableAlert.getFlapping(),
ruleUrl: ruleUrl?.absoluteUrl,
};
if (executableAlert.isAlertAsData()) {
transformActionParamsOptions.aadAlert = executableAlert.getAlertAsData();
}
const actionToRun = {
...action,
params: injectActionParams({
actionTypeId,
ruleUrl,
ruleName: this.rule.name,
actionParams: transformActionParams({
actionsPlugin,
alertId: ruleId,
alertType: this.ruleType.id,
actionTypeId,
alertName: this.rule.name,
spaceId,
tags: this.rule.tags,
alertInstanceId: alert.getId(),
alertUuid: alert.getUuid(),
alertActionGroup: actionGroup,
alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!,
context: alert.getContext(),
actionId: action.id,
state: alert.getState(),
kibanaBaseUrl: this.taskRunnerContext.kibanaBaseUrl,
alertParams: this.rule.params,
actionParams: action.params,
flapping: alert.getFlapping(),
ruleUrl: ruleUrl?.absoluteUrl,
}),
actionParams: transformActionParams(transformActionParamsOptions),
}),
};
@ -570,7 +581,6 @@ export class ExecutionHandler<
for (const action of this.rule.actions) {
const alertsArray = Object.entries(alerts);
let summarizedAlerts = null;
if (this.shouldGetSummarizedAlerts({ action, throttledSummaryActions })) {
summarizedAlerts = await this.getSummarizedAlerts({
action,
@ -634,6 +644,15 @@ export class ExecutionHandler<
continue;
}
if (summarizedAlerts) {
const alertAsData = summarizedAlerts.all.data.find(
(alertHit: AlertHit) => alertHit._id === alert.getUuid()
);
if (alertAsData) {
alert.setAlertAsData(alertAsData);
}
}
if (action.group === actionGroup && !this.isAlertMuted(alertId)) {
if (
this.isRecoveredAlert(action.group) ||
@ -667,12 +686,13 @@ export class ExecutionHandler<
}
return false;
}
if (action.useAlertDataForTemplate) {
return true;
}
// we fetch summarizedAlerts to filter alerts in memory as well
if (!isSummaryAction(action) && !action.alertsFilter) {
return false;
}
if (
isSummaryAction(action) &&
isSummaryActionThrottled({

View file

@ -6,6 +6,7 @@
*/
import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
import { AADAlert } from '@kbn/alerts-as-data-utils';
import { mapKeys, snakeCase } from 'lodash/fp';
import {
RuleActionParams,
@ -15,7 +16,7 @@ import {
SanitizedRule,
} from '../types';
interface TransformActionParamsOptions {
export interface TransformActionParamsOptions {
actionsPlugin: ActionsPluginStartContract;
alertId: string;
alertType: string;
@ -35,6 +36,7 @@ interface TransformActionParamsOptions {
context: AlertInstanceContext;
ruleUrl?: string;
flapping: boolean;
aadAlert?: AADAlert;
}
interface SummarizedAlertsWithAll {
@ -76,40 +78,45 @@ export function transformActionParams({
alertParams,
ruleUrl,
flapping,
aadAlert,
}: TransformActionParamsOptions): RuleActionParams {
// when the list of variables we pass in here changes,
// the UI will need to be updated as well; see:
// x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts
const variables = {
alertId,
alertName,
spaceId,
tags,
alertInstanceId,
alertActionGroup,
alertActionGroupName,
context,
date: new Date().toISOString(),
state,
kibanaBaseUrl,
params: alertParams,
rule: {
params: alertParams,
id: alertId,
name: alertName,
type: alertType,
spaceId,
tags,
url: ruleUrl,
},
alert: {
id: alertInstanceId,
uuid: alertUuid,
actionGroup: alertActionGroup,
actionGroupName: alertActionGroupName,
flapping,
},
};
const variables =
aadAlert !== undefined
? aadAlert
: {
alertId,
alertName,
spaceId,
tags,
alertInstanceId,
alertActionGroup,
alertActionGroupName,
context,
date: new Date().toISOString(),
state,
kibanaBaseUrl,
params: alertParams,
rule: {
params: alertParams,
id: alertId,
name: alertName,
type: alertType,
spaceId,
tags,
url: ruleUrl,
},
alert: {
id: alertInstanceId,
uuid: alertUuid,
actionGroup: alertActionGroup,
actionGroupName: alertActionGroupName,
flapping,
},
};
return actionsPlugin.renderActionParameterTemplates(
actionTypeId,
actionId,

View file

@ -12,7 +12,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues;
* This object is then used to validate and parse the value entered.
*/
export const allowedExperimentalValues = Object.freeze({
isMustacheAutocompleteOn: false,
isMustacheAutocompleteOn: true,
sentinelOneConnectorOn: false,
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiComboBox, EuiButtonEmpty, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -15,8 +15,6 @@ import {
TextAreaWithMessageVariables,
} from '@kbn/triggers-actions-ui-plugin/public';
import { EmailActionParams } from '../types';
import { getIsExperimentalFeatureEnabled } from '../../common/get_experimental_features';
import { TextAreaWithAutocomplete } from '../../components/text_area_with_autocomplete';
const noop = () => {};
@ -32,8 +30,8 @@ export const EmailParamsFields = ({
onBlur = noop,
showEmailSubjectAndMessage = true,
useDefaultMessage,
ruleTypeId,
}: ActionParamsProps<EmailActionParams>) => {
const isMustacheAutocompleteOn = getIsExperimentalFeatureEnabled('isMustacheAutocompleteOn');
const { to, cc, bcc, subject, message } = actionParams;
const toOptions = to ? to.map((label: string) => ({ label })) : [];
const ccOptions = cc ? cc.map((label: string) => ({ label })) : [];
@ -64,10 +62,6 @@ export const EmailParamsFields = ({
const isBCCInvalid: boolean =
errors.bcc !== undefined && errors.bcc.length > 0 && bcc !== undefined;
const TextAreaComponent = useMemo(() => {
return isMustacheAutocompleteOn ? TextAreaWithAutocomplete : TextAreaWithMessageVariables;
}, [isMustacheAutocompleteOn]);
return (
<>
<EuiFormRow
@ -239,7 +233,7 @@ export const EmailParamsFields = ({
</EuiFormRow>
)}
{showEmailSubjectAndMessage && (
<TextAreaComponent
<TextAreaWithMessageVariables
index={index}
editAction={editAction}
messageVariables={messageVariables}

View file

@ -13,7 +13,6 @@ import { ActionConnectorMode } from '@kbn/triggers-actions-ui-plugin/public';
import { MockCodeEditor } from '@kbn/triggers-actions-ui-plugin/public/application/code_editor.mock';
import { OpsgenieSubActions } from '../../../common';
import type { OpsgenieActionParams } from '../../../server/connector_types';
const kibanaReactPath = '../../../../../../src/plugins/kibana_react/public';
jest.mock(kibanaReactPath, () => {

View file

@ -37,7 +37,7 @@ export const ServerLogParamsFields: React.FunctionComponent<
editAction('level', 'info', index);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [actionParams.level]);
const [[isUsingDefault, defaultMessageUsed], setDefaultMessageUsage] = useState<
[boolean, string | undefined]

View file

@ -16,6 +16,7 @@ import { IToasts } from '@kbn/core/public';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { getConnectorType as getSlackConnectorType } from './slack';
import { getSlackApiConnectorType } from '../slack_api';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
@ -51,6 +52,14 @@ const { loadActionTypes } = jest.requireMock(
'@kbn/triggers-actions-ui-plugin/public/application/lib/action_connector_api/connector_types'
);
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
});
// GET api/actions/connector_types?feature_id=alerting
loadActionTypes.mockResolvedValue([
@ -95,6 +104,7 @@ actionTypeRegistry.register(getSlackApiConnectorType());
const baseProps = {
actions: [],
defaultActionGroupId: 'metrics.inventory_threshold.fired',
ruleTypeId: 'metrics.inventory_threshold',
hasAlertsMappings: true,
featureId: 'alerting',
recoveryActionGroup: 'recovered',
@ -170,7 +180,9 @@ describe('ActionForm - Slack API Connector', () => {
render(
<IntlProvider locale="en">
<ActionForm {...testProps} />
<QueryClientProvider client={queryClient}>
<ActionForm {...testProps} />
</QueryClientProvider>
</IntlProvider>
);

View file

@ -8,7 +8,7 @@
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
"server/**/*.json",
"common/**/*",
"public/**/*"
"public/**/*",
],
"kbn_references": [
"@kbn/core",
@ -33,7 +33,6 @@
"@kbn/core-saved-objects-common",
"@kbn/core-http-browser-mocks",
"@kbn/core-saved-objects-api-server-mocks",
"@kbn/alerts-ui-shared",
"@kbn/alerting-plugin",
"@kbn/securitysolution-ecs",
"@kbn/ui-theme",

View file

@ -19,6 +19,8 @@ export const allowedExperimentalValues = Object.freeze({
rulesDetailLogs: true,
ruleUseExecutionStatus: false,
ruleKqlBar: false,
isMustacheAutocompleteOn: false,
showMustacheAutocompleteSwitch: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
@ -29,7 +31,8 @@ const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<Experimen
/**
* Parses the string value used in `xpack.trigger_actions_ui.enableExperimental` kibana configuration,
* which should be a string of values delimited by a comma (`,`)
* which should be a string of values delimited by a comma (`,`):
* xpack.trigger_actions_ui.enableExperimental: ['ruleStatusFilter', 'ruleTagFilter']
*
* @param configValue
* @throws TriggersActionsUIInvalidExperimentalValue

View file

@ -22,8 +22,8 @@ import {
import { ActionVariable } from '@kbn/alerting-plugin/common';
import { AddMessageVariables } from '@kbn/alerts-ui-shared';
import { euiThemeVars } from '@kbn/ui-theme';
import { filterSuggestions } from '../lib/filter_suggestions_for_autocomplete';
import { templateActionVariable } from '../lib/template_action_variable';
import { filterSuggestions } from './lib/filter_suggestions_for_autocomplete';
import { templateActionVariable } from './lib/template_action_variable';
export interface TextAreaWithAutocompleteProps {
editAction: (property: string, value: any, index: number) => void;
@ -291,7 +291,6 @@ export const TextAreaWithAutocomplete: React.FunctionComponent<TextAreaWithAutoc
}
}, [editAction, index, inputTargetValue, isListOpen, paramsProperty]);
const onClick = useCallback(() => closeList(), [closeList]);
const onScroll = useCallback(
(evt) => {
// FUTURE ENGINEER -> we need to make sure to not close the autocomplete option list

View file

@ -9,6 +9,8 @@ import React, { useState } from 'react';
import { EuiTextArea, EuiFormRow } from '@elastic/eui';
import { ActionVariable } from '@kbn/alerting-plugin/common';
import { AddMessageVariables } from '@kbn/alerts-ui-shared';
import { getIsExperimentalFeatureEnabled } from '../../common/get_experimental_features';
import { TextAreaWithAutocomplete } from './text_area_with_autocomplete';
import { templateActionVariable } from '../lib';
interface Props {
@ -23,7 +25,7 @@ interface Props {
errors?: string[];
}
export const TextAreaWithMessageVariables: React.FunctionComponent<Props> = ({
const TextAreaWithMessageVariablesLegacy: React.FunctionComponent<Props> = ({
messageVariables,
paramsProperty,
index,
@ -87,3 +89,15 @@ export const TextAreaWithMessageVariables: React.FunctionComponent<Props> = ({
</EuiFormRow>
);
};
export const TextAreaWithMessageVariables = (props: Props) => {
let isMustacheAutocompleteOn;
try {
isMustacheAutocompleteOn = getIsExperimentalFeatureEnabled('isMustacheAutocompleteOn');
} catch (e) {
isMustacheAutocompleteOn = false;
}
if (isMustacheAutocompleteOn) return TextAreaWithAutocomplete(props);
return TextAreaWithMessageVariablesLegacy(props);
};

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { HttpStart } from '@kbn/core-http-browser';
import { DataViewField } from '@kbn/data-views-plugin/common';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
import { ActionVariable } from '@kbn/alerting-plugin/common';
import { useEffect, useMemo, useState } from 'react';
import { EcsFlat } from '@kbn/ecs';
import { EcsMetadata } from '@kbn/alerts-as-data-utils/src/field_maps/types';
import { isEmpty } from 'lodash';
export const getDescription = (fieldName: string, ecsFlat: Record<string, EcsMetadata>) => {
let ecsField = ecsFlat[fieldName];
if (isEmpty(ecsField?.description ?? '') && fieldName.includes('kibana.alert.')) {
ecsField = ecsFlat[fieldName.replace('kibana.alert.', '')];
}
return ecsField?.description ?? '';
};
async function loadRuleTypeAadTemplateFields({
http,
ruleTypeId,
}: {
http: HttpStart;
ruleTypeId: string;
}): Promise<DataViewField[]> {
if (!ruleTypeId || !http) return [];
const fields = await http.get<DataViewField[]>(`${BASE_RAC_ALERTS_API_PATH}/aad_fields`, {
query: { ruleTypeId },
});
return fields;
}
export function useRuleTypeAadTemplateFields(
http: HttpStart,
ruleTypeId: string | undefined,
enabled: boolean
): { isLoading: boolean; fields: ActionVariable[] } {
// Reimplement useQuery here; this hook is sometimes called in contexts without a QueryClientProvider
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<DataViewField[]>([]);
useEffect(() => {
if (enabled && data.length === 0 && ruleTypeId) {
setIsLoading(true);
loadRuleTypeAadTemplateFields({ http, ruleTypeId }).then((res) => {
setData(res);
setIsLoading(false);
});
}
}, [data, enabled, http, ruleTypeId]);
return useMemo(
() => ({
isLoading,
fields: data.map<ActionVariable>((d) => ({
name: d.name,
description: getDescription(d.name, EcsFlat),
})),
}),
[data, isLoading]
);
}

View file

@ -75,6 +75,7 @@ describe('cloneRule', () => {
"level": "info",
"message": "alert ",
},
"useAlertDataForTemplate": undefined,
"uuid": "123456",
},
],

View file

@ -16,11 +16,13 @@ const transformAction: RewriteRequestCase<RuleAction> = ({
params,
frequency,
alerts_filter: alertsFilter,
use_alert_data_for_template: useAlertDataForTemplate,
}) => ({
group,
id,
params,
actionTypeId,
useAlertDataForTemplate,
...(frequency
? {
frequency: {

View file

@ -27,17 +27,20 @@ const rewriteBodyRequest: RewriteResponseCase<RuleCreateBody> = ({
}): any => ({
...res,
rule_type_id: ruleTypeId,
actions: actions.map(({ group, id, params, frequency, alertsFilter }) => ({
group,
id,
params,
frequency: {
notify_when: frequency!.notifyWhen,
throttle: frequency!.throttle,
summary: frequency!.summary,
},
alerts_filter: alertsFilter,
})),
actions: actions.map(
({ group, id, params, frequency, alertsFilter, useAlertDataForTemplate }) => ({
group,
id,
params,
frequency: {
notify_when: frequency!.notifyWhen,
throttle: frequency!.throttle,
summary: frequency!.summary,
},
alerts_filter: alertsFilter,
use_alert_data_for_template: useAlertDataForTemplate,
})
),
});
export async function createRule({

View file

@ -17,18 +17,21 @@ type RuleUpdatesBody = Pick<
>;
const rewriteBodyRequest: RewriteResponseCase<RuleUpdatesBody> = ({ actions, ...res }): any => ({
...res,
actions: actions.map(({ group, id, params, frequency, uuid, alertsFilter }) => ({
group,
id,
params,
frequency: {
notify_when: frequency!.notifyWhen,
throttle: frequency!.throttle,
summary: frequency!.summary,
},
alerts_filter: alertsFilter,
...(uuid && { uuid }),
})),
actions: actions.map(
({ group, id, params, frequency, uuid, alertsFilter, useAlertDataForTemplate }) => ({
group,
id,
params,
frequency: {
notify_when: frequency!.notifyWhen,
throttle: frequency!.throttle,
summary: frequency!.summary,
},
alerts_filter: alertsFilter,
use_alert_data_for_template: useAlertDataForTemplate,
...(uuid && { uuid }),
})
),
});
export async function updateRule({

View file

@ -60,6 +60,7 @@ export interface ActionAccordionFormProps {
defaultActionMessage?: string;
setActionIdByIndex: (id: string, index: number) => void;
setActionGroupIdByIndex?: (group: string, index: number) => void;
setActionUseAlertDataForTemplate?: (enabled: boolean, index: number) => void;
setActions: (actions: RuleAction[]) => void;
setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void;
setActionFrequencyProperty: (key: string, value: RuleActionParam, index: number) => void;
@ -70,6 +71,7 @@ export interface ActionAccordionFormProps {
) => void;
featureId: string;
producerId: string;
ruleTypeId?: string;
messageVariables?: ActionVariables;
summaryMessageVariables?: ActionVariables;
setHasActionsDisabled?: (value: boolean) => void;
@ -84,7 +86,6 @@ export interface ActionAccordionFormProps {
minimumThrottleInterval?: [number | undefined, string];
notifyWhenSelectOptions?: NotifyWhenSelectOptions[];
defaultRuleFrequency?: RuleActionFrequency;
ruleTypeId?: string;
hasFieldsForAAD?: boolean;
disableErrorMessages?: boolean;
}
@ -99,6 +100,7 @@ export const ActionForm = ({
defaultActionGroupId,
setActionIdByIndex,
setActionGroupIdByIndex,
setActionUseAlertDataForTemplate,
setActions,
setActionParamsProperty,
setActionFrequencyProperty,
@ -437,6 +439,7 @@ export const ActionForm = ({
actionConnector={actionConnector}
index={index}
key={`action-form-action-at-${actionItem.uuid}`}
setActionUseAlertDataForTemplate={setActionUseAlertDataForTemplate}
setActionParamsProperty={setActionParamsProperty}
setActionFrequencyProperty={setActionFrequencyProperty}
setActionAlertsFilterProperty={setActionAlertsFilterProperty}

View file

@ -64,6 +64,19 @@ jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
useUiSetting: jest.fn().mockImplementation((_, defaultValue) => defaultValue),
}));
jest.mock('../../../common/get_experimental_features', () => ({
getIsExperimentalFeatureEnabled() {
return true;
},
}));
jest.mock('../../hooks/use_rule_aad_template_fields', () => ({
useRuleTypeAadTemplateFields: () => ({
isLoading: false,
fields: [],
}),
}));
describe('action_type_form', () => {
afterEach(() => {
jest.clearAllMocks();
@ -402,6 +415,59 @@ describe('action_type_form', () => {
]);
});
it('clears the default message when the user toggles the "Use template fields from alerts index" switch ', async () => {
const setActionParamsProperty = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: '.pagerduty',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
actionParamsFields: mockedActionParamsFields,
defaultActionParams: {
dedupKey: '{{rule.id}}:{{alert.id}}',
eventAction: 'resolve',
},
});
actionTypeRegistry.get.mockReturnValue(actionType);
const wrapper = render(
<IntlProvider locale="en">
{getActionTypeForm({
index: 1,
ruleTypeId: 'test',
setActionParamsProperty,
actionItem: {
id: '123',
actionTypeId: '.pagerduty',
group: 'recovered',
params: {
eventAction: 'recovered',
dedupKey: '232323',
summary: '2323',
source: 'source',
severity: '1',
timestamp: new Date().toISOString(),
component: 'test',
group: 'group',
class: 'test class',
},
},
})}
</IntlProvider>
);
expect(wrapper.getByTestId('mustacheAutocompleteSwitch')).toBeTruthy();
await act(async () => {
wrapper.getByTestId('mustacheAutocompleteSwitch').click();
});
expect(setActionParamsProperty).toHaveBeenCalledWith('dedupKey', '', 1);
});
describe('Customize notify when options', () => {
it('should not have "On status changes" notify when option for summary actions', async () => {
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
@ -523,6 +589,7 @@ function getActionTypeForm({
onAddConnector,
onDeleteAction,
onConnectorSelected,
setActionParamsProperty,
setActionFrequencyProperty,
setActionAlertsFilterProperty,
hasAlertsMappings = true,
@ -530,6 +597,7 @@ function getActionTypeForm({
summaryMessageVariables = { context: [], state: [], params: [] },
notifyWhenSelectOptions,
defaultNotifyWhenValue,
ruleTypeId,
}: {
index?: number;
actionConnector?: ActionConnector<Record<string, unknown>, Record<string, unknown>>;
@ -541,12 +609,14 @@ function getActionTypeForm({
onDeleteAction?: () => void;
onConnectorSelected?: (id: string) => void;
setActionFrequencyProperty?: () => void;
setActionParamsProperty?: () => void;
setActionAlertsFilterProperty?: () => void;
hasAlertsMappings?: boolean;
messageVariables?: ActionVariables;
summaryMessageVariables?: ActionVariables;
notifyWhenSelectOptions?: NotifyWhenSelectOptions[];
defaultNotifyWhenValue?: RuleNotifyWhenType;
ruleTypeId?: string;
}) {
const actionConnectorDefault = {
actionTypeId: '.pagerduty',
@ -628,7 +698,7 @@ function getActionTypeForm({
onDeleteAction={onDeleteAction ?? jest.fn()}
onConnectorSelected={onConnectorSelected ?? jest.fn()}
defaultActionGroupId={defaultActionGroupId ?? 'default'}
setActionParamsProperty={jest.fn()}
setActionParamsProperty={setActionParamsProperty ?? jest.fn()}
setActionFrequencyProperty={setActionFrequencyProperty ?? jest.fn()}
setActionAlertsFilterProperty={setActionAlertsFilterProperty ?? jest.fn()}
index={index ?? 1}
@ -641,6 +711,7 @@ function getActionTypeForm({
defaultNotifyWhenValue={defaultNotifyWhenValue}
producerId="infrastructure"
featureId="infrastructure"
ruleTypeId={ruleTypeId}
/>
);
}

View file

@ -29,6 +29,7 @@ import {
EuiSplitPanel,
useEuiTheme,
EuiCallOut,
EuiSwitch,
} from '@elastic/eui';
import { isEmpty, partition, some } from 'lodash';
import {
@ -42,6 +43,8 @@ import {
getDurationUnitValue,
parseDuration,
} from '@kbn/alerting-plugin/common/parse_duration';
import { SavedObjectAttribute } from '@kbn/core-saved-objects-api-server';
import { getIsExperimentalFeatureEnabled } from '../../../common/get_experimental_features';
import { betaBadgeProps } from './beta_badge_props';
import {
IErrorObject,
@ -64,6 +67,7 @@ import { validateParamsForWarnings } from '../../lib/validate_params_for_warning
import { ActionAlertsFilterTimeframe } from './action_alerts_filter_timeframe';
import { ActionAlertsFilterQuery } from './action_alerts_filter_query';
import { validateActionFilterQuery } from '../../lib/value_validators';
import { useRuleTypeAadTemplateFields } from '../../hooks/use_rule_aad_template_fields';
export type ActionTypeFormProps = {
actionItem: RuleAction;
@ -72,6 +76,7 @@ export type ActionTypeFormProps = {
onAddConnector: () => void;
onConnectorSelected: (id: string) => void;
onDeleteAction: () => void;
setActionUseAlertDataForTemplate?: (enabled: boolean, index: number) => void;
setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void;
setActionFrequencyProperty: (key: string, value: RuleActionParam, index: number) => void;
setActionAlertsFilterProperty: (
@ -120,6 +125,7 @@ export const ActionTypeForm = ({
onAddConnector,
onConnectorSelected,
onDeleteAction,
setActionUseAlertDataForTemplate,
setActionParamsProperty,
setActionFrequencyProperty,
setActionAlertsFilterProperty,
@ -148,7 +154,7 @@ export const ActionTypeForm = ({
}: ActionTypeFormProps) => {
const {
application: { capabilities },
http: { basePath },
http,
} = useKibana().services;
const { euiTheme } = useEuiTheme();
const [isOpen, setIsOpen] = useState(true);
@ -178,6 +184,53 @@ export const ActionTypeForm = ({
const isSummaryAction = actionItem.frequency?.summary;
const [useAadTemplateFields, setUseAadTemplateField] = useState(
actionItem?.useAlertDataForTemplate ?? false
);
const [storedActionParamsForAadToggle, setStoredActionParamsForAadToggle] = useState<
Record<string, SavedObjectAttribute>
>({});
const { fields: aadTemplateFields } = useRuleTypeAadTemplateFields(
http,
ruleTypeId,
useAadTemplateFields
);
const templateFields = useMemo(
() => (useAadTemplateFields ? aadTemplateFields : availableActionVariables),
[aadTemplateFields, availableActionVariables, useAadTemplateFields]
);
let showMustacheAutocompleteSwitch;
try {
showMustacheAutocompleteSwitch =
getIsExperimentalFeatureEnabled('showMustacheAutocompleteSwitch') && ruleTypeId;
} catch (e) {
showMustacheAutocompleteSwitch = false;
}
const handleUseAadTemplateFields = useCallback(() => {
setUseAadTemplateField((prevVal) => {
if (setActionUseAlertDataForTemplate) {
setActionUseAlertDataForTemplate(!prevVal, index);
}
return !prevVal;
});
const currentActionParams = { ...actionItem.params };
for (const key of Object.keys(currentActionParams)) {
setActionParamsProperty(key, storedActionParamsForAadToggle[key] ?? '', index);
}
setStoredActionParamsForAadToggle(currentActionParams);
}, [
setActionUseAlertDataForTemplate,
storedActionParamsForAadToggle,
setStoredActionParamsForAadToggle,
setActionParamsProperty,
actionItem.params,
index,
]);
const getDefaultParams = async () => {
const connectorType = await actionTypeRegistry.get(actionItem.actionTypeId);
let defaultParams;
@ -227,9 +280,15 @@ export const ActionTypeForm = ({
const defaultParams = await getDefaultParams();
if (defaultParams) {
for (const [key, paramValue] of Object.entries(defaultParams)) {
const defaultAADParams: typeof defaultParams = {};
if (actionItem.params[key] === undefined || actionItem.params[key] === null) {
setActionParamsProperty(key, paramValue, index);
// Add default param to AAD defaults only if it does not contain any template code
if (typeof paramValue !== 'string' || !paramValue.match(/{{.*?}}/g)) {
defaultAADParams[key] = paramValue;
}
}
setStoredActionParamsForAadToggle(defaultAADParams);
}
}
})();
@ -240,9 +299,14 @@ export const ActionTypeForm = ({
(async () => {
const defaultParams = await getDefaultParams();
if (defaultParams && actionGroup) {
const defaultAADParams: typeof defaultParams = {};
for (const [key, paramValue] of Object.entries(defaultParams)) {
setActionParamsProperty(key, paramValue, index);
if (!paramValue.match(/{{.*?}}/g)) {
defaultAADParams[key] = paramValue;
}
}
setStoredActionParamsForAadToggle(defaultAADParams);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -273,6 +337,12 @@ export const ActionTypeForm = ({
})();
}, [actionItem, disableErrorMessages]);
useEffect(() => {
if (isEmpty(storedActionParamsForAadToggle) && actionItem.params.subAction) {
setStoredActionParamsForAadToggle(actionItem.params);
}
}, [actionItem.params, storedActionParamsForAadToggle]);
const canSave = hasSaveActionsCapability(capabilities);
const actionGroupDisplay = (
@ -463,39 +533,54 @@ export const ActionTypeForm = ({
<EuiSplitPanel.Inner color="plain">
{ParamsFieldsComponent ? (
<EuiErrorBoundary>
<Suspense fallback={null}>
<ParamsFieldsComponent
actionParams={actionItem.params as any}
index={index}
errors={actionParamsErrors.errors}
editAction={(key: string, value: RuleActionParam, i: number) => {
setWarning(
validateParamsForWarnings(
value,
basePath.publicBaseUrl,
availableActionVariables
)
);
setActionParamsProperty(key, value, i);
}}
messageVariables={availableActionVariables}
defaultMessage={
// if action is a summary action, show the default summary message
isSummaryAction
? defaultSummaryMessage
: selectedActionGroup?.defaultActionMessage ?? defaultActionMessage
}
useDefaultMessage={useDefaultMessage}
actionConnector={actionConnector}
executionMode={ActionConnectorMode.ActionForm}
/>
{warning ? (
<>
<EuiSpacer size="s" />
<EuiCallOut size="s" color="warning" title={warning} />
</>
) : null}
</Suspense>
<EuiFlexGroup gutterSize="m" direction="column">
{showMustacheAutocompleteSwitch && (
<EuiFlexItem>
<EuiSwitch
label="Use template fields from alerts index"
checked={useAadTemplateFields}
onChange={handleUseAadTemplateFields}
data-test-subj="mustacheAutocompleteSwitch"
/>
</EuiFlexItem>
)}
<EuiFlexItem>
<Suspense fallback={null}>
<ParamsFieldsComponent
actionParams={actionItem.params as any}
errors={actionParamsErrors.errors}
index={index}
editAction={(key: string, value: RuleActionParam, i: number) => {
setWarning(
validateParamsForWarnings(
value,
http.basePath.publicBaseUrl,
availableActionVariables
)
);
setActionParamsProperty(key, value, i);
}}
messageVariables={templateFields}
defaultMessage={
// if action is a summary action, show the default summary message
isSummaryAction
? defaultSummaryMessage
: selectedActionGroup?.defaultActionMessage ?? defaultActionMessage
}
useDefaultMessage={useDefaultMessage}
actionConnector={actionConnector}
executionMode={ActionConnectorMode.ActionForm}
ruleTypeId={ruleTypeId}
/>
{warning ? (
<>
<EuiSpacer size="s" />
<EuiCallOut size="s" color="warning" title={warning} />
</>
) : null}
</Suspense>
</EuiFlexItem>
</EuiFlexGroup>
</EuiErrorBoundary>
) : null}
</EuiSplitPanel.Inner>

View file

@ -839,6 +839,9 @@ export const RuleForm = ({
: { ...actionGroup, defaultActionMessage: ruleTypeModel?.defaultActionMessage }
)}
recoveryActionGroup={recoveryActionGroup}
setActionUseAlertDataForTemplate={(enabled: boolean, index: number) => {
setActionProperty('useAlertDataForTemplate', enabled, index);
}}
setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)}
setActionGroupIdByIndex={(group: string, index: number) =>
setActionProperty('group', group, index)

View file

@ -217,6 +217,7 @@ export interface ActionParamsProps<TParams> {
index: number;
editAction: (key: string, value: RuleActionParam, index: number) => void;
errors: IErrorObject;
ruleTypeId?: string;
messageVariables?: ActionVariable[];
defaultMessage?: string;
useDefaultMessage?: boolean;

View file

@ -55,7 +55,8 @@
"@kbn/dashboard-plugin",
"@kbn/licensing-plugin",
"@kbn/expressions-plugin",
"@kbn/serverless",
"@kbn/core-saved-objects-api-server",
"@kbn/serverless"
],
"exclude": ["target/**/*"]
}

View file

@ -102,6 +102,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
group: 'default',
params: {},
uuid: response.body.actions[0].uuid,
use_alert_data_for_template: false,
},
],
enabled: true,

View file

@ -116,6 +116,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
params: {},
connector_type_id: 'test.noop',
uuid: response.body.rules[0].actions[0].uuid,
use_alert_data_for_template: false,
},
]);
expect(response.statusCode).to.eql(200);

View file

@ -78,6 +78,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
group: 'default',
params: {},
uuid: response.body.actions[0].uuid,
use_alert_data_for_template: false,
},
],
enabled: true,
@ -181,6 +182,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
group: 'default',
params: {},
uuid: response.body.actions[0].uuid,
use_alert_data_for_template: false,
},
{
id: 'my-slack1',
@ -190,6 +192,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
message: 'something important happened!',
},
uuid: response.body.actions[1].uuid,
use_alert_data_for_template: false,
},
{
id: 'system-connector-test.system-action',
@ -197,6 +200,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
connector_type_id: 'test.system-action',
params: {},
uuid: response.body.actions[2].uuid,
use_alert_data_for_template: false,
},
],
enabled: true,