{!isSubjectDisabled ? (
diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js
new file mode 100644
index 000000000..1997017e7
--- /dev/null
+++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js
@@ -0,0 +1,275 @@
+import React, { useEffect, useState } from 'react';
+import {
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiPopover,
+ EuiComboBox,
+ EuiButtonIcon,
+ EuiExpression,
+} from '@elastic/eui';
+import * as _ from 'lodash';
+import { FormikFormRow, FormikInputWrapper } from '../../../../components/FormControls';
+
+const ExpressionQuery = ({
+ selections,
+ dataTestSubj,
+ onChange,
+ value,
+ defaultText,
+ label,
+ name = 'expressionQueries',
+}) => {
+ const DEFAULT_DESCRIPTION = defaultText ? defaultText : 'Select';
+ const OPERATORS = ['AND', 'OR', 'NOT'];
+ const [usedExpressions, setUsedExpressions] = useState([]);
+
+ useEffect(() => {
+ let expressions = [];
+ if (value?.length) {
+ let values = [...value];
+ if (OPERATORS.indexOf(values[0]?.description) === -1) values = ['', ...values];
+
+ let counter = 0;
+ values.map((exp, idx) => {
+ if (idx % 2 === 0) {
+ expressions.push({
+ description: exp.description,
+ isOpen: false,
+ monitor_name: '',
+ monitor_id: '',
+ });
+ counter++;
+ } else {
+ const currentIndex = idx - counter;
+ expressions[currentIndex] = { ...expressions[currentIndex], ...exp };
+ }
+ });
+ } else {
+ expressions = [];
+ }
+
+ setUsedExpressions(expressions);
+ }, []);
+
+ const getValue = (expressions) =>
+ expressions.map((exp) => ({
+ condition: _.toLower(exp.description),
+ monitor_id: exp.monitor_id,
+ monitor_name: exp.name,
+ }));
+
+ const changeMonitor = (selection, exp, idx, form) => {
+ const expressions = _.cloneDeep(usedExpressions);
+ expressions[idx] = {
+ ...expressions[idx],
+ monitor_id: selection[0].monitor_id,
+ monitor_name: selection[0].label,
+ };
+
+ setUsedExpressions(expressions);
+ onBlur(form, expressions);
+ };
+
+ const changeCondition = (selection, exp, idx, form) => {
+ const expressions = _.cloneDeep(usedExpressions);
+ expressions[idx] = { ...expressions[idx], description: selection[0].description };
+ setUsedExpressions(expressions);
+ onBlur(form, expressions);
+ };
+
+ const onBlur = (form, expressions) => {
+ form.setFieldTouched('expressionQueries', true);
+ form.setFieldValue('triggerDefinitions[0].triggerConditions', getValue(expressions));
+ form.setFieldError('expressionQueries', validate());
+ };
+
+ const openPopover = (idx) => {
+ const expressions = _.cloneDeep(usedExpressions);
+ expressions[idx] = { ...expressions[idx], isOpen: !expressions[idx].isOpen };
+ setUsedExpressions(expressions);
+ };
+
+ const closePopover = (idx) => {
+ const expressions = _.cloneDeep(usedExpressions);
+ expressions[idx] = { ...expressions[idx], isOpen: false };
+ setUsedExpressions(expressions);
+ };
+
+ const renderOptions = (expression, idx, form) => (
+
+
+ changeCondition(selection, expression, idx, form)}
+ options={[
+ { description: '', label: '' },
+ { description: 'AND', label: 'AND' },
+ { description: 'OR', label: 'OR' },
+ { description: 'NOT', label: 'NOT' },
+ ]}
+ />
+
+ {renderMonitorOptions(expression, idx, form)}
+
+ {
+ const usedExp = _.cloneDeep(usedExpressions);
+ usedExp.splice(idx, 1);
+ usedExp.length && (usedExp[0].description = '');
+ setUsedExpressions([...usedExp]);
+ }}
+ iconType={'trash'}
+ color="danger"
+ aria-label={'Remove condition'}
+ style={{ marginTop: '4px' }}
+ />
+
+
+ );
+
+ const renderMonitorOptions = (expression, idx, form) => (
+
changeMonitor(selection, expression, idx, form)}
+ selectedOptions={[
+ {
+ label: expression.monitor_name,
+ monitor_id: expression.monitor_id,
+ },
+ ]}
+ style={{ width: '250px' }}
+ options={(() => {
+ const differences = _.differenceBy(selections, usedExpressions, 'monitor_id');
+ return [
+ {
+ monitor_id: expression.monitor_id,
+ label: expression.monitor_name,
+ },
+ ...differences.map((sel) => ({
+ monitor_id: sel.monitor_id,
+ label: sel.label,
+ })),
+ ];
+ })()}
+ />
+ );
+
+ const isValid = () => usedExpressions.length > 1;
+
+ const validate = () => {
+ if (!isValid()) return 'At least two monitors should be selected.';
+ };
+
+ return (
+ validate(),
+ }}
+ render={({ field, form }) => (
+ form.touched['expressionQueries'] && !isValid(),
+ error: () => validate(),
+ }}
+ >
+
+ {!usedExpressions.length && (
+
+ onBlur(form, usedExpressions)}
+ />
+ }
+ isOpen={false}
+ panelPaddingSize="s"
+ anchorPosition="rightDown"
+ closePopover={() => onBlur(form, usedExpressions)}
+ />
+
+ )}
+ {usedExpressions.map((expression, idx) => (
+
+ {
+ e.preventDefault();
+ openPopover(idx);
+ }}
+ />
+ }
+ isOpen={expression.isOpen}
+ closePopover={() => closePopover(idx)}
+ panelPaddingSize="s"
+ anchorPosition="rightDown"
+ >
+ {renderOptions(expression, idx, form)}
+
+
+ ))}
+ {selections.length > usedExpressions.length && (
+
+ {
+ const expressions = _.cloneDeep(usedExpressions);
+ const differences = _.differenceBy(selections, expressions, 'monitor_id');
+ const newExpressions = [
+ ...expressions,
+ {
+ description: usedExpressions.length ? 'AND' : '',
+ isOpen: false,
+ monitor_name: differences[0]?.label,
+ monitor_id: differences[0]?.monitor_id,
+ },
+ ];
+
+ setUsedExpressions(newExpressions);
+ onBlur(form, newExpressions);
+ }}
+ color={'primary'}
+ iconType="plusInCircleFilled"
+ aria-label={'Add one more condition'}
+ data-test-subj={'condition-add-selection-btn'}
+ style={{ marginTop: '1px' }}
+ />
+
+ )}
+
+
+ )}
+ />
+ );
+};
+
+export default ExpressionQuery;
diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts
new file mode 100644
index 000000000..49e1b709c
--- /dev/null
+++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+
+describe(' spec', () => {
+ it('renders the component', () => {
+ const tree = render();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js
index dfae41837..7d34ad1f2 100644
--- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js
+++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js
@@ -35,6 +35,8 @@ export function formikToTriggerDefinition(values, monitorUiMetadata) {
return formikToBucketLevelTrigger(values, monitorUiMetadata);
case MONITOR_TYPE.DOC_LEVEL:
return formikToDocumentLevelTrigger(values, monitorUiMetadata);
+ case MONITOR_TYPE.COMPOSITE_LEVEL:
+ return formikToCompositeLevelTrigger(values, monitorUiMetadata);
default:
return formikToQueryLevelTrigger(values, monitorUiMetadata);
}
@@ -86,6 +88,20 @@ export function formikToDocumentLevelTrigger(values, monitorUiMetadata) {
};
}
+export function formikToCompositeLevelTrigger(values, monitorUiMetadata) {
+ const condition = formikToCompositeTriggerCondition(values, monitorUiMetadata);
+ const actions = formikToCompositeTriggerAction(values);
+ return {
+ chained_alert_trigger: {
+ id: values.id,
+ name: values.name,
+ severity: values.severity,
+ condition: condition,
+ actions: actions,
+ },
+ };
+}
+
export function formikToDocumentLevelTriggerCondition(values, monitorUiMetadata) {
const triggerConditions = _.get(values, 'triggerConditions', []);
const searchType = _.get(monitorUiMetadata, 'search.searchType', SEARCH_TYPE.QUERY);
@@ -99,6 +115,28 @@ export function formikToDocumentLevelTriggerCondition(values, monitorUiMetadata)
};
}
+export function formikToCompositeTriggerCondition(values, monitorUiMetadata) {
+ const conditionMap = {
+ and: '&&',
+ or: '||',
+ not: '!',
+ '': '',
+ };
+
+ const triggerConditions = _.get(values, 'triggerConditions', []);
+ const source = triggerConditions.reduce((query, expression) => {
+ query += ` ${conditionMap[expression.condition]} (monitor[id=${expression.monitor_id}])`;
+ return query.trim();
+ }, '');
+
+ return {
+ script: {
+ lang: 'painless',
+ source: source,
+ },
+ };
+}
+
export function getDocumentLevelScriptSource(conditions) {
const scriptSourceContents = [];
conditions.forEach((condition) => {
@@ -171,6 +209,51 @@ export function formikToBucketLevelTriggerAction(values) {
return actions;
}
+export function formikToCompositeTriggerAction(values) {
+ const actions = values.actions;
+ const executionPolicyPath = 'action_execution_policy.action_execution_scope';
+ if (actions && actions.length > 0) {
+ return actions.map((action) => {
+ let formattedAction = _.cloneDeep(action);
+
+ switch (formattedAction.throttle_enabled) {
+ case true:
+ _.set(formattedAction, 'throttle.unit', FORMIK_INITIAL_ACTION_VALUES.throttle.unit);
+ break;
+ case false:
+ formattedAction = _.omit(formattedAction, ['throttle']);
+ break;
+ }
+
+ const notifyOption = _.get(formattedAction, `${executionPolicyPath}`);
+ const notifyOptionId = _.isString(notifyOption) ? notifyOption : _.keys(notifyOption)[0];
+ switch (notifyOptionId) {
+ case NOTIFY_OPTIONS_VALUES.PER_ALERT:
+ const actionableAlerts = _.get(
+ formattedAction,
+ `${executionPolicyPath}.${NOTIFY_OPTIONS_VALUES.PER_ALERT}.actionable_alerts`,
+ []
+ );
+ _.set(
+ formattedAction,
+ `${executionPolicyPath}.${NOTIFY_OPTIONS_VALUES.PER_ALERT}.actionable_alerts`,
+ actionableAlerts.map((entry) => entry.value)
+ );
+ break;
+ case NOTIFY_OPTIONS_VALUES.PER_EXECUTION:
+ _.set(
+ formattedAction,
+ `${executionPolicyPath}.${NOTIFY_OPTIONS_VALUES.PER_EXECUTION}`,
+ {}
+ );
+ break;
+ }
+ return formattedAction;
+ });
+ }
+ return actions;
+}
+
export function formikToTriggerUiMetadata(values, monitorUiMetadata) {
switch (monitorUiMetadata.monitor_type) {
case MONITOR_TYPE.QUERY_LEVEL:
@@ -221,6 +304,13 @@ export function formikToTriggerUiMetadata(values, monitorUiMetadata) {
docLevelTriggersUiMetadata[trigger.name] = triggerMetadata;
});
return docLevelTriggersUiMetadata;
+
+ case MONITOR_TYPE.COMPOSITE_LEVEL:
+ const compositeTriggersUiMetadata = {};
+ _.get(values, 'triggerDefinitions', []).forEach((trigger) => {
+ compositeTriggersUiMetadata[trigger.name] = _.get(trigger, 'triggerConditions', []);
+ });
+ return compositeTriggersUiMetadata;
}
}
diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js
new file mode 100644
index 000000000..b5d40400a
--- /dev/null
+++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js
@@ -0,0 +1,38 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Fragment } from 'react';
+import DefineCompositeLevelTrigger from './DefineCompositeLevelTrigger';
+
+const CompositeMonitorsAlertTrigger = ({
+ edit,
+ triggerArrayHelpers,
+ monitor,
+ monitorValues,
+ triggerValues,
+ isDarkMode,
+ httpClient,
+ notifications,
+ notificationService,
+ plugins,
+}) => {
+ return (
+
+
+
+ );
+};
+export default CompositeMonitorsAlertTrigger;
diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js
new file mode 100644
index 000000000..035817ff0
--- /dev/null
+++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js
@@ -0,0 +1,169 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import _ from 'lodash';
+import { EuiAccordion, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import { FormikFieldText, FormikSelect } from '../../../../components/FormControls';
+import { hasError, isInvalid } from '../../../../utils/validate';
+import { DEFAULT_TRIGGER_NAME, SEVERITY_OPTIONS } from '../../utils/constants';
+import { validateTriggerName } from '../DefineTrigger/utils/validation';
+import ExpressionQuery from '../../components/ExpressionQuery/ExpressionQuery';
+import TriggerNotifications from './TriggerNotifications';
+import ContentPanel from '../../../../components/ContentPanel';
+import { FORMIK_INITIAL_TRIGGER_VALUES } from '../CreateTrigger/utils/constants';
+
+const defaultRowProps = {
+ label: 'Trigger name',
+ style: { paddingLeft: '10px' },
+ isInvalid,
+ error: hasError,
+};
+
+const defaultInputProps = { isInvalid };
+
+const selectFieldProps = {
+ validate: () => {},
+};
+
+const selectRowProps = {
+ label: 'Severity level',
+ style: { paddingLeft: '10px', marginTop: '0px' },
+ isInvalid,
+ error: hasError,
+};
+
+const selectInputProps = {
+ options: SEVERITY_OPTIONS,
+};
+
+const propTypes = {
+ monitorValues: PropTypes.object.isRequired,
+ triggerValues: PropTypes.object.isRequired,
+ isDarkMode: PropTypes.bool.isRequired,
+};
+
+export const titleTemplate = (title, subTitle) => (
+
+ {title}
+ {subTitle && (
+
+ {subTitle}
+
+ )}
+
+
+);
+
+class DefineCompositeLevelTrigger extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+
+ render() {
+ const {
+ edit,
+ monitorValues,
+ triggers,
+ triggerValues,
+ isDarkMode,
+ httpClient,
+ notifications,
+ notificationService,
+ plugins,
+ selectedMonitors,
+ } = this.props;
+ const fieldPath = `triggerDefinitions[0].`;
+ const triggerName = _.get(triggerValues, `${fieldPath}name`, DEFAULT_TRIGGER_NAME);
+ const triggerDefinitions = _.get(triggerValues, 'triggerDefinitions', []);
+ _.set(triggerValues, 'triggerDefinitions', [
+ {
+ ...FORMIK_INITIAL_TRIGGER_VALUES,
+ ...triggerDefinitions[0],
+ severity: 1,
+ name: triggerName,
+ },
+ ]);
+
+ const monitorList = monitorValues?.associatedMonitors
+ ? monitorValues.associatedMonitors?.map((monitor) => ({
+ label: monitor.label.replaceAll(' ', '_'),
+ monitor_id: monitor.value,
+ }))
+ : [];
+
+ return (
+
+
+
+
+
+
+
+ [monitor, { description: 'and' }]))
+ .slice(0, -1)}
+ onChange={() => {}}
+ dataTestSubj={'composite_expression_query'}
+ defaultText={'Select associated monitor'}
+ />
+
+
+
+ {titleTemplate('Alert severity')}
+
+
+
+
+
+
+ );
+ }
+}
+
+DefineCompositeLevelTrigger.propTypes = propTypes;
+
+export default DefineCompositeLevelTrigger;
diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js
new file mode 100644
index 000000000..aedb55566
--- /dev/null
+++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js
@@ -0,0 +1,149 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Fragment } from 'react';
+import {
+ EuiButton,
+ EuiSpacer,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+} from '@elastic/eui';
+import { titleTemplate } from './DefineCompositeLevelTrigger';
+import Message from '../../components/Action/actions';
+import { DEFAULT_MESSAGE_SOURCE, FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants';
+import { getTriggerContext } from '../../utils/helper';
+import { formikToMonitor } from '../../../CreateMonitor/containers/CreateMonitor/utils/formikToMonitor';
+import _ from 'lodash';
+import { formikToTrigger } from '../CreateTrigger/utils/formikToTrigger';
+import { MONITOR_TYPE } from '../../../../utils/constants';
+import { TRIGGER_TYPE } from '../CreateTrigger/utils/constants';
+import { backendErrorNotification } from '../../../../utils/helpers';
+import { checkForError } from '../ConfigureActions/ConfigureActions';
+
+const NotificationConfigDialog = ({
+ channel,
+ closeModal,
+ triggerValues,
+ httpClient,
+ notifications,
+}) => {
+ const triggerIndex = 0;
+ const monitor = formikToMonitor(triggerValues);
+ const context = getTriggerContext({}, monitor, triggerValues, 0);
+
+ const initialActionValues = _.cloneDeep(FORMIK_INITIAL_ACTION_VALUES);
+ let action = _.get(triggerValues, 'triggerDefinitions[0].actions[0]', {
+ ...initialActionValues,
+ });
+
+ const sendTestMessage = async (index) => {
+ const flattenedDestinations = [];
+ // TODO: For bucket-level triggers, sendTestMessage will only send a test message if there is
+ // at least one bucket of data from the monitor input query.
+ let testTrigger = _.cloneDeep(
+ formikToTrigger(triggerValues, monitor.ui_metadata)[triggerIndex]
+ );
+ let action;
+ let condition;
+
+ switch (monitor.monitor_type) {
+ case MONITOR_TYPE.BUCKET_LEVEL:
+ action = _.get(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions[${index}]`);
+ condition = {
+ ..._.get(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.condition`),
+ buckets_path: { _count: '_count' },
+ script: {
+ source: 'params._count >= 0',
+ },
+ };
+ _.set(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions`, [action]);
+ _.set(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.condition`, condition);
+ break;
+ case MONITOR_TYPE.DOC_LEVEL:
+ action = _.get(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.actions[${index}]`);
+ condition = {
+ ..._.get(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.condition`),
+ script: { lang: 'painless', source: 'return true' },
+ };
+ _.set(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.actions`, [action]);
+ _.set(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.condition`, condition);
+ break;
+ default:
+ action = _.get(testTrigger, `actions[${index}]`);
+ condition = {
+ ..._.get(testTrigger, 'condition'),
+ script: { lang: 'painless', source: 'return true' },
+ };
+ _.set(testTrigger, 'actions', [action]);
+ _.set(testTrigger, 'condition', condition);
+ break;
+ }
+
+ const testMonitor = { ...monitor, triggers: [{ ...testTrigger }] };
+
+ try {
+ const response = await httpClient.post('../api/alerting/monitors/_execute', {
+ query: { dryrun: false },
+ body: JSON.stringify(testMonitor),
+ });
+ let error = null;
+ if (response.ok) {
+ error = checkForError(response, error);
+ if (!_.isEmpty(action.destination_id)) {
+ const destinationName = _.get(
+ _.find(flattenedDestinations, { value: action.destination_id }),
+ 'label'
+ );
+ notifications.toasts.addSuccess(`Test message sent to "${destinationName}."`);
+ }
+ }
+ if (error || !response.ok) {
+ const errorMessage = error == null ? response.resp : error;
+ console.error('There was an error trying to send test message', errorMessage);
+ backendErrorNotification(notifications, 'send', 'test message', errorMessage);
+ }
+ } catch (err) {
+ console.error('There was an error trying to send test message', err);
+ }
+ };
+
+ console.log('ACTION', action);
+ return (
+ closeModal()}>
+
+
+ Configure notification
+
+
+
+ {titleTemplate('Customize message')}
+
+
+
+
+
+
+ closeModal()}>Close
+ closeModal()} fill>
+ Update
+
+
+
+ );
+};
+
+export default NotificationConfigDialog;
diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js
new file mode 100644
index 000000000..67eb337af
--- /dev/null
+++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js
@@ -0,0 +1,140 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Fragment, useState, useEffect } from 'react';
+import {
+ EuiSpacer,
+ EuiButton,
+ EuiText,
+ EuiAccordion,
+ EuiHorizontalRule,
+ EuiButtonIcon,
+} from '@elastic/eui';
+import TriggerNotificationsContent from './TriggerNotificationsContent';
+import { titleTemplate } from './DefineCompositeLevelTrigger';
+import { MAX_CHANNELS_RESULT_SIZE, OS_NOTIFICATION_PLUGIN } from '../../../../utils/constants';
+import { CHANNEL_TYPES } from '../../utils/constants';
+
+const TriggerNotifications = ({
+ httpClient,
+ actions = [],
+ plugins,
+ notifications,
+ notificationService,
+ triggerValues,
+}) => {
+ const [channels, setChannels] = useState([]);
+ const [options, setOptions] = useState([]);
+
+ useEffect(() => {
+ let newChannels = [...actions];
+ if (_.isEmpty(newChannels))
+ newChannels = [
+ {
+ name: '',
+ id: '',
+ },
+ ];
+ setChannels(newChannels);
+
+ getChannels().then((channels) => setOptions(channels));
+ }, []);
+
+ const getChannels = async () => {
+ const hasNotificationPlugin = plugins.indexOf(OS_NOTIFICATION_PLUGIN) !== -1;
+
+ let channels = [];
+ let index = 0;
+ const getChannels = async () => {
+ const getChannelsQuery = {
+ from_index: index,
+ max_items: MAX_CHANNELS_RESULT_SIZE,
+ config_type: CHANNEL_TYPES,
+ sort_field: 'name',
+ sort_order: 'asc',
+ };
+
+ const channelsResponse = await notificationService.getChannels(getChannelsQuery);
+
+ channels = channels.concat(
+ channelsResponse.items.map((channel) => ({
+ label: channel.name,
+ value: channel.config_id,
+ type: channel.config_type,
+ description: channel.description,
+ }))
+ );
+
+ if (channelsResponse.total && channels.length < channelsResponse.total) {
+ index += MAX_CHANNELS_RESULT_SIZE;
+ await getChannels();
+ }
+ };
+
+ if (hasNotificationPlugin) {
+ await getChannels();
+ }
+
+ return channels;
+ };
+
+ const onAddNotification = () => {
+ const newChannels = [...channels];
+ newChannels.push({
+ label: '',
+ value: '',
+ });
+ setChannels(newChannels);
+ };
+
+ const onRemoveNotification = (idx) => {
+ const newChannels = [...channels];
+ newChannels.splice(idx, 1);
+ setChannels(newChannels);
+ };
+
+ return (
+
+ {titleTemplate('Notifications')}
+
+ {channels.length &&
+ channels.map((channel, idx) => (
+ {`Notification ${idx + 1}`}}
+ paddingSize={'s'}
+ extraAction={
+ channels.length > 1 && (
+ onRemoveNotification(idx)}
+ size={'s'}
+ />
+ )
+ }
+ >
+
+
+ ))}
+
+ onAddNotification()}>Add notification
+
+ );
+};
+
+export default TriggerNotifications;
diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js
new file mode 100644
index 000000000..3fd2dabb5
--- /dev/null
+++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js
@@ -0,0 +1,91 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Fragment, useState } from 'react';
+import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
+import { FormikComboBox } from '../../../../components/FormControls';
+import NotificationConfigDialog from './NotificationConfigDialog';
+import _ from 'lodash';
+import { FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants';
+
+const TriggerNotificationsContent = ({
+ channel,
+ options,
+ idx,
+ triggerValues,
+ httpClient,
+ notifications,
+}) => {
+ const [selected, setSelected] = useState([]);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+
+ const onChange = (selectedOptions) => {
+ setSelected(selectedOptions);
+ const initialActionValues = _.cloneDeep(FORMIK_INITIAL_ACTION_VALUES);
+ _.set(triggerValues, 'triggerDefinitions[0].actions[0]', initialActionValues);
+ };
+
+ const showConfig = (channels) => setIsModalVisible(true);
+
+ return (
+
+
+
+
+ onChange(selectedOptions),
+ singleSelection: { asPlainText: true },
+ }}
+ />
+
+
+
+ Manage channels
+
+
+
+ {selected.length ? (
+
+
+ showConfig()}>
+ Configure notification
+
+
+ ) : null}
+
+ {isModalVisible && (
+ setIsModalVisible(false)}
+ channel={channel}
+ triggerValues={triggerValues}
+ httpClient={httpClient}
+ notifications={notifications}
+ />
+ )}
+
+ );
+};
+
+export default TriggerNotificationsContent;
diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/index.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/index.js
new file mode 100644
index 000000000..dff1a2d91
--- /dev/null
+++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/index.js
@@ -0,0 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import DefineCompositeLevelTrigger from './DefineCompositeLevelTrigger';
+
+export default DefineCompositeLevelTrigger;
diff --git a/public/utils/constants.js b/public/utils/constants.js
index 9511ae0d2..cfe51489c 100644
--- a/public/utils/constants.js
+++ b/public/utils/constants.js
@@ -30,6 +30,7 @@ export const MONITOR_TYPE = {
BUCKET_LEVEL: 'bucket_level_monitor',
CLUSTER_METRICS: 'cluster_metrics_monitor',
DOC_LEVEL: 'doc_level_monitor',
+ COMPOSITE_LEVEL: 'composite',
};
export const DESTINATION_ACTIONS = {
diff --git a/server/clusters/alerting/alertingPlugin.js b/server/clusters/alerting/alertingPlugin.js
index 8564d0020..46cde5562 100644
--- a/server/clusters/alerting/alertingPlugin.js
+++ b/server/clusters/alerting/alertingPlugin.js
@@ -46,6 +46,14 @@ export default function alertingPlugin(Client, config, components) {
method: 'POST',
});
+ alerting.createWorkflow = ca({
+ url: {
+ fmt: `${API_ROUTE_PREFIX}/workflows?refresh=wait_for`,
+ },
+ needBody: true,
+ method: 'POST',
+ });
+
alerting.deleteMonitor = ca({
url: {
fmt: `${MONITOR_BASE_API}/<%=monitorId%>`,
diff --git a/server/routes/monitors.js b/server/routes/monitors.js
index 6cc4967c0..a447327df 100644
--- a/server/routes/monitors.js
+++ b/server/routes/monitors.js
@@ -45,6 +45,16 @@ export default function (services, router) {
monitorService.createMonitor
);
+ router.post(
+ {
+ path: '/api/alerting/workflows',
+ validate: {
+ body: schema.any(),
+ },
+ },
+ monitorService.createWorkflow
+ );
+
router.post(
{
path: '/api/alerting/monitors/_execute',
diff --git a/server/services/MonitorService.js b/server/services/MonitorService.js
index 81d788520..0ed7554ac 100644
--- a/server/services/MonitorService.js
+++ b/server/services/MonitorService.js
@@ -35,6 +35,28 @@ export default class MonitorService {
}
};
+ createWorkflow = async (context, req, res) => {
+ try {
+ const params = { body: req.body };
+ const { callAsCurrentUser } = await this.esDriver.asScoped(req);
+ const createResponse = await callAsCurrentUser('alerting.createWorkflow', params);
+ return res.ok({
+ body: {
+ ok: true,
+ resp: createResponse,
+ },
+ });
+ } catch (err) {
+ console.error('Alerting - MonitorService - createWorkflow:', err);
+ return res.ok({
+ body: {
+ ok: false,
+ resp: err.message,
+ },
+ });
+ }
+ };
+
deleteMonitor = async (context, req, res) => {
try {
const { id } = req.params;
diff --git a/yarn.lock b/yarn.lock
index 4c675f913..d35b800d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -232,90 +232,6 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
-"@node-rs/xxhash-android-arm-eabi@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-android-arm-eabi/-/xxhash-android-arm-eabi-1.4.0.tgz#55ace4d3882686d1e379aaf613e1338d78f13fc8"
- integrity sha512-JuZNqt5/znWkIGteikQdS+HT9S0JsMYi06S4yzU/sMKLCIPvD0MnCTXlYtuDcgRIKScCaepAsSQVomnAyLFNNA==
-
-"@node-rs/xxhash-android-arm64@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-android-arm64/-/xxhash-android-arm64-1.4.0.tgz#2290c53ceabda804afb4c45679613d833a6385a0"
- integrity sha512-BZzQO5jlgsIr9HhiqTwZjYqlfVeZiu+7PaoAdNEOq+i/SjyAqv1jGSkyek4rBSAiodyNkXcbE0eQtomeN6a55w==
-
-"@node-rs/xxhash-darwin-arm64@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-darwin-arm64/-/xxhash-darwin-arm64-1.4.0.tgz#96df4f48b13deb6899e84ed0882bdbd0a4856f13"
- integrity sha512-JlEAzTsQaqJaWVse/JP//6QKBIhzqzTlvNY4uEbi8TaZMfvDDhW//ClXM6CkSV799GJxAYPu1LXa4+OeBQpa7Q==
-
-"@node-rs/xxhash-darwin-x64@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-darwin-x64/-/xxhash-darwin-x64-1.4.0.tgz#9df3ca3a87354dd5386aadfa20ad032a299c2b8f"
- integrity sha512-9ycVJfzLvw1wc6Tgq0giLkMn5nGOBawTeOA17t27dQFdY/scZPz583DO7w+eznMnlzUXwoLiloanUebRhy+piQ==
-
-"@node-rs/xxhash-freebsd-x64@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-freebsd-x64/-/xxhash-freebsd-x64-1.4.0.tgz#24b0c0bfd33429303688b4af78f9d323daa0fb5b"
- integrity sha512-vFRDr6qA0gHWQDjuSxXcdzM4Ppk+5VebEhc76zkWrRVc6RG60fxLo5B4j6QwMwXGTYaG8HMv/nQhAgbnOCWWxQ==
-
-"@node-rs/xxhash-linux-arm-gnueabihf@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm-gnueabihf/-/xxhash-linux-arm-gnueabihf-1.4.0.tgz#4c09f70cd39429fb1a52f3567085e949603d4817"
- integrity sha512-0KS6y1caqbtPanos9XNMekWpozCHA6QSlQzaZyn9Hn+Z+mYpR5+NoWixefhp06jt59qF9+LkkF3C9fSEHYmq/w==
-
-"@node-rs/xxhash-linux-arm64-gnu@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm64-gnu/-/xxhash-linux-arm64-gnu-1.4.0.tgz#e92d7026614506fb4db309977127fd8589fabd7c"
- integrity sha512-QI97JK2qiQhVgRtUBMgA1ZjPLpwnz11SE2Mw1jryejmyH9EXKKiCyt2FweO6MVP7bEuMxcdajBho4pEL7s/QsA==
-
-"@node-rs/xxhash-linux-arm64-musl@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm64-musl/-/xxhash-linux-arm64-musl-1.4.0.tgz#a8b16233a86c116e6af32a69278248d17b2d09e7"
- integrity sha512-dtMid4OMkNBYGJkjoT1jdkENpV8m8MGp3lliDN8C+2znZUQM8KFRTXRkfaq4lgzu3Y2XeYzsLOoBsBd3Hgf7gA==
-
-"@node-rs/xxhash-linux-x64-gnu@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-x64-gnu/-/xxhash-linux-x64-gnu-1.4.0.tgz#385ec91396ebaa2b73abf419be3971ec893dcbd1"
- integrity sha512-OeOQL10cG62wL1IVoeC74xESmefHU7r3xiZMTP2hK5Dh3FdF2sa3x/Db9BcGXlaokg/lMGDxuTuzOLC2Rv/wlQ==
-
-"@node-rs/xxhash-linux-x64-musl@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-x64-musl/-/xxhash-linux-x64-musl-1.4.0.tgz#715bb962502b0ec69e1fc19db22ac035c63d30c7"
- integrity sha512-kZ8wNi5bH9b+ZpuPlSbFd6JXk8CKbfCvCPZ0Vk0IqLkzB6PihQflnZPM9r0QZ2jtFgyfWmpbFK4YxwX9YcyLog==
-
-"@node-rs/xxhash-win32-arm64-msvc@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-arm64-msvc/-/xxhash-win32-arm64-msvc-1.4.0.tgz#4a3a4ebcb50c73e4309e429b28eb44dbf8f7f71f"
- integrity sha512-Ggv66jlhQvj4XgQqNgl2JKQ7My/97PvPZi5jKbcS7t65wJC36J6XERQwRPdupO8UH63XfPqb7HJqrgmiz8tmlA==
-
-"@node-rs/xxhash-win32-ia32-msvc@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-ia32-msvc/-/xxhash-win32-ia32-msvc-1.4.0.tgz#fdfdb43e41113a8baf15779ca53bb637d2e1bc8f"
- integrity sha512-mYpF1+7unqKKGsPn7Y8X6SqP2Bc5BU5dsHBKhAGAuvrMg9W63zM+YWM8/fpNGfFlOrjiKRvXHZ96nrZyzoxeBw==
-
-"@node-rs/xxhash-win32-x64-msvc@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-x64-msvc/-/xxhash-win32-x64-msvc-1.4.0.tgz#aee714a4ae0121f3947f94139adf13f5b6d93d12"
- integrity sha512-rKuqWHuQNlrfjIOkQW3oCBta/GUlyVoUkKB13aVr8uixOs/eneuDaYJx2h02FAAWlWCKADFnMxgDl0LVFBy53w==
-
-"@node-rs/xxhash@^1.3.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@node-rs/xxhash/-/xxhash-1.4.0.tgz#1e75850e0e530c9224e8e5ba4056d52e8868291b"
- integrity sha512-UpSOParhMqbQ7hsYovN2e+uqvWqHJiCDvFl8gDzMcXgBY/PkI2zo2zhdRAZdz48c6/dke+0WjCKy90wDVQxS6g==
- optionalDependencies:
- "@node-rs/xxhash-android-arm-eabi" "1.4.0"
- "@node-rs/xxhash-android-arm64" "1.4.0"
- "@node-rs/xxhash-darwin-arm64" "1.4.0"
- "@node-rs/xxhash-darwin-x64" "1.4.0"
- "@node-rs/xxhash-freebsd-x64" "1.4.0"
- "@node-rs/xxhash-linux-arm-gnueabihf" "1.4.0"
- "@node-rs/xxhash-linux-arm64-gnu" "1.4.0"
- "@node-rs/xxhash-linux-arm64-musl" "1.4.0"
- "@node-rs/xxhash-linux-x64-gnu" "1.4.0"
- "@node-rs/xxhash-linux-x64-musl" "1.4.0"
- "@node-rs/xxhash-win32-arm64-msvc" "1.4.0"
- "@node-rs/xxhash-win32-ia32-msvc" "1.4.0"
- "@node-rs/xxhash-win32-x64-msvc" "1.4.0"
-
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301"
@@ -2952,7 +2868,7 @@ loader-runner@^2.4.0:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
-loader-utils@1.4.2, loader-utils@^2.0.4:
+loader-utils@1.4.2, loader-utils@^1.2.3:
version "1.4.2"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3"
integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==
@@ -4491,12 +4407,11 @@ tapable@^1.0.0, tapable@^1.1.3:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
-"terser-webpack-plugin@npm:@amoo-miki/terser-webpack-plugin@1.4.5-rc.2":
- version "1.4.5-rc.2"
- resolved "https://registry.yarnpkg.com/@amoo-miki/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5-rc.2.tgz#046c062ef22c126c2544718674bc6624e3651b9c"
- integrity sha512-JFSGSzsWgSHEqQXlnHDh3gw+jdVdVlWM2Irdps9P/yWYNY/5VjuG8sdoW4mbuP8/HM893/k8N+ipbeqsd8/xpA==
+terser-webpack-plugin@^1.4.3:
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b"
+ integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==
dependencies:
- "@node-rs/xxhash" "^1.3.0"
cacache "^12.0.2"
find-cache-dir "^2.1.0"
is-wsl "^1.1.0"
@@ -4789,12 +4704,11 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1:
source-list-map "^2.0.0"
source-map "~0.6.1"
-"webpack@npm:@amoo-miki/webpack@4.46.0-rc.2":
- version "4.46.0-rc.2"
- resolved "https://registry.yarnpkg.com/@amoo-miki/webpack/-/webpack-4.46.0-rc.2.tgz#36824597c14557a7bb0a8e13203e30275e7b02bd"
- integrity sha512-Y/ZqxTHOoDF1kz3SR63Y9SZGTDUpZNNFrisTRHofWhP8QvNX3LMN+TCmEP56UfLaiLVKMcaiFjx8kFb2TgyBaQ==
+webpack@^4.41.5:
+ version "4.46.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542"
+ integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==
dependencies:
- "@node-rs/xxhash" "^1.3.0"
"@webassemblyjs/ast" "1.9.0"
"@webassemblyjs/helper-module-context" "1.9.0"
"@webassemblyjs/wasm-edit" "1.9.0"
@@ -4807,7 +4721,7 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1:
eslint-scope "^4.0.3"
json-parse-better-errors "^1.0.2"
loader-runner "^2.4.0"
- loader-utils "^2.0.4"
+ loader-utils "^1.2.3"
memory-fs "^0.4.1"
micromatch "^3.1.10"
mkdirp "^0.5.3"
@@ -4815,7 +4729,7 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1:
node-libs-browser "^2.2.1"
schema-utils "^1.0.0"
tapable "^1.1.3"
- terser-webpack-plugin "npm:@amoo-miki/terser-webpack-plugin@1.4.5-rc.2"
+ terser-webpack-plugin "^1.4.3"
watchpack "^1.7.4"
webpack-sources "^1.4.1"