From beb1a8b275a6ae49a6b434d9a0762223ceb3c527 Mon Sep 17 00:00:00 2001 From: Nishtha Mehrotra Date: Wed, 18 Dec 2024 15:51:59 -0800 Subject: [PATCH 1/4] Updated Preview Message experience for notifications in detector trigger setup --- public/components/Commons/Constants.tsx | 12 ++++ .../Notifications/NotificationForm.tsx | 49 +++++++++---- .../AlertCondition/AlertConditionPanel.tsx | 29 +++++++- .../AlertConditionPanel.test.tsx.snap | 70 +++++++++---------- 4 files changed, 107 insertions(+), 53 deletions(-) create mode 100644 public/components/Commons/Constants.tsx diff --git a/public/components/Commons/Constants.tsx b/public/components/Commons/Constants.tsx new file mode 100644 index 00000000..bf670a39 --- /dev/null +++ b/public/components/Commons/Constants.tsx @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export const DEFAULT_MESSAGE_SOURCE = { + MESSAGE_BODY: `- Triggered alert condition: {{ctx.trigger.name}} + - Severity: {{ctx.trigger.severity}} + - Threat detector: {{ctx.detector.name}} + - Description: {{ctx.detector.description}} + - Detector data sources: {{ctx.detector.datasources}} + `.trim(), +}; diff --git a/public/components/Notifications/NotificationForm.tsx b/public/components/Notifications/NotificationForm.tsx index 06354941..fd4a95d5 100644 --- a/public/components/Notifications/NotificationForm.tsx +++ b/public/components/Notifications/NotificationForm.tsx @@ -15,7 +15,7 @@ import { EuiSpacer, EuiCompressedSwitch, EuiText, - EuiCompressedTextArea, + EuiCompressedTextArea, EuiCompressedCheckbox, EuiLink, } from '@elastic/eui'; import React, { useState } from 'react'; import { NOTIFICATIONS_HREF } from '../../utils/constants'; @@ -26,6 +26,8 @@ import { TriggerAction, } from '../../../types'; import { getIsNotificationPluginInstalled } from '../../utils/helpers'; +import Mustache from 'mustache'; +import { DEFAULT_MESSAGE_SOURCE } from '../Commons/Constants'; export interface NotificationFormProps { allNotificationChannels: NotificationChannelTypeOptions[]; @@ -37,10 +39,12 @@ export interface NotificationFormProps { onMessageBodyChange: (message: string) => void; onMessageSubjectChange: (subject: string) => void; onNotificationToggle?: (enabled: boolean) => void; + context: any } export const NotificationForm: React.FC = ({ action, + context, allNotificationChannels, loadingNotifications, prepareMessage, @@ -53,6 +57,8 @@ export const NotificationForm: React.FC = ({ const hasNotificationPlugin = getIsNotificationPluginInstalled(); const [shouldSendNotification, setShouldSendNotification] = useState(!!action?.destination_id); const selectedNotificationChannelOption: NotificationChannelOption[] = []; + const onDisplayPreviewChange = (e) => setDisplayPreview(e.target.checked); + const [displayPreview, setDisplayPreview] = useState(false); if (shouldSendNotification && action?.destination_id) { allNotificationChannels.forEach((typeOption) => { const matchingChannel = typeOption.options.find( @@ -61,6 +67,14 @@ export const NotificationForm: React.FC = ({ if (matchingChannel) selectedNotificationChannelOption.push(matchingChannel); }); } + let preview = ''; + try { + console.log(context) + preview = Mustache.render(action?.message_template.source, context); + } catch (err) { + preview = err.message; + console.error('There was an error rendering mustache template', err); + } return ( <> @@ -168,18 +182,27 @@ export const NotificationForm: React.FC = ({ /> - {prepareMessage && ( - - - prepareMessage(true /* updateMessage */)} - > - Generate message - - - - )} + + + onDisplayPreviewChange(e)} + /> + + + {displayPreview ? ( + + + + ) : null} diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx index 62707764..f3713cbf 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx @@ -31,6 +31,7 @@ import { } from '../../../../../../../types'; import { NotificationForm } from '../../../../../../components/Notifications/NotificationForm'; import { ALERT_SEVERITY_OPTIONS } from '../../../../../../utils/constants'; +import { DEFAULT_MESSAGE_SOURCE } from '../../../../../../components/Commons/Constants'; interface AlertConditionPanelProps extends RouteComponentProps { alertCondition: AlertCondition; @@ -99,6 +100,26 @@ export default class AlertConditionPanel extends Component< }); } + getTriggerContext = () => { + const lineBreakAndTab = '\n\t'; + const { alertCondition, detector } = this.props; + const detectorInput = detector.inputs[0].detector_input; + const detectorIndices = `${lineBreakAndTab}${detectorInput.indices.join( + `,${lineBreakAndTab}` + )}`; + return { + trigger: { + name:alertCondition.name, + severity: parseAlertSeverityToOption(alertCondition.severity)?.label || alertCondition.severity + }, + detector: { + name: detector.name, + description: detectorInput.description, + datasources: detectorIndices + } + }; + }; + // When component mounts, we prepare message but at this point we don't want to emit the // trigger changed metric since it is not user initiated. So we use the onMount flag to determine that // and pass it downstream accordingly. @@ -113,7 +134,7 @@ export default class AlertConditionPanel extends Component< parseAlertSeverityToOption(alertCondition.severity)?.label || alertCondition.severity }`; const detectorName = `Threat detector: ${detector.name}`; - const defaultSubject = [alertConditionName, alertConditionSeverity, detectorName].join(' - '); + const defaultSubject = 'Alerting Notification action'; if (updateMessage || !alertCondition.actions[0]?.subject_template.source) this.onMessageSubjectChange(defaultSubject, !onMount); @@ -157,7 +178,7 @@ export default class AlertConditionPanel extends Component< if (alertConditionSelections.length) defaultMessageBody = defaultMessageBody + lineBreak + lineBreak + alertConditionSelections.join(lineBreak); - this.onMessageBodyChange(defaultMessageBody, !onMount); + this.onMessageBodyChange(DEFAULT_MESSAGE_SOURCE.MESSAGE_BODY, !onMount); } }; @@ -286,6 +307,7 @@ export default class AlertConditionPanel extends Component< }; render() { + const context = this.getTriggerContext(); const { alertCondition = getEmptyAlertCondition(), allNotificationChannels, @@ -537,6 +559,9 @@ export default class AlertConditionPanel extends Component<
+
+
+ Preview message +
+
@@ -1707,30 +1704,27 @@ Object { class="euiFlexItem" >
+
+
+ Preview message +
+
From 6c24361e6036e83ba23d9b7a8a04de9658612f93 Mon Sep 17 00:00:00 2001 From: Nishtha Mehrotra Date: Thu, 19 Dec 2024 10:25:29 -0800 Subject: [PATCH 2/4] Updated detector notification template --- public/components/Commons/Constants.tsx | 2 ++ public/components/Notifications/NotificationForm.tsx | 7 ++----- .../components/AlertCondition/AlertConditionPanel.tsx | 3 ++- .../__snapshots__/AlertConditionPanel.test.tsx.snap | 6 ------ 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/public/components/Commons/Constants.tsx b/public/components/Commons/Constants.tsx index bf670a39..e9c29f43 100644 --- a/public/components/Commons/Constants.tsx +++ b/public/components/Commons/Constants.tsx @@ -9,4 +9,6 @@ export const DEFAULT_MESSAGE_SOURCE = { - Description: {{ctx.detector.description}} - Detector data sources: {{ctx.detector.datasources}} `.trim(), + MESSAGE_SUBJECT: `Triggered alert condition: {{ctx.trigger.name}} - Severity: {{ctx.trigger.severity}} - Threat detector: {{ctx.detector.name}} + `.trim(), }; diff --git a/public/components/Notifications/NotificationForm.tsx b/public/components/Notifications/NotificationForm.tsx index fd4a95d5..1cf520ec 100644 --- a/public/components/Notifications/NotificationForm.tsx +++ b/public/components/Notifications/NotificationForm.tsx @@ -15,7 +15,7 @@ import { EuiSpacer, EuiCompressedSwitch, EuiText, - EuiCompressedTextArea, EuiCompressedCheckbox, EuiLink, + EuiCompressedTextArea, EuiCompressedCheckbox, } from '@elastic/eui'; import React, { useState } from 'react'; import { NOTIFICATIONS_HREF } from '../../utils/constants'; @@ -27,7 +27,6 @@ import { } from '../../../types'; import { getIsNotificationPluginInstalled } from '../../utils/helpers'; import Mustache from 'mustache'; -import { DEFAULT_MESSAGE_SOURCE } from '../Commons/Constants'; export interface NotificationFormProps { allNotificationChannels: NotificationChannelTypeOptions[]; @@ -69,8 +68,7 @@ export const NotificationForm: React.FC = ({ } let preview = ''; try { - console.log(context) - preview = Mustache.render(action?.message_template.source, context); + preview = `${Mustache.render(action?.subject_template.source, context)}\n\n${Mustache.render(action?.message_template.source, context)}`; } catch (err) { preview = err.message; console.error('There was an error rendering mustache template', err); @@ -191,7 +189,6 @@ export const NotificationForm: React.FC = ({ onChange={(e) => onDisplayPreviewChange(e)} /> - {displayPreview ? ( -
@@ -1722,9 +1719,6 @@ Object { -
From 00d2e7ffd27c4e9e09d6eef320ea63541e11209f Mon Sep 17 00:00:00 2001 From: Nishtha Mehrotra Date: Fri, 20 Dec 2024 12:10:04 -0800 Subject: [PATCH 3/4] updated code review comments --- public/components/Commons/Constants.tsx | 14 ------- .../Notifications/NotificationForm.tsx | 40 +++++++++++-------- .../AlertCondition/AlertConditionPanel.tsx | 13 +++--- .../AlertConditionPanel.test.tsx.snap | 8 ++-- public/utils/constants.ts | 13 ++++++ types/Alert.ts | 12 ++++++ 6 files changed, 58 insertions(+), 42 deletions(-) delete mode 100644 public/components/Commons/Constants.tsx diff --git a/public/components/Commons/Constants.tsx b/public/components/Commons/Constants.tsx deleted file mode 100644 index e9c29f43..00000000 --- a/public/components/Commons/Constants.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -export const DEFAULT_MESSAGE_SOURCE = { - MESSAGE_BODY: `- Triggered alert condition: {{ctx.trigger.name}} - - Severity: {{ctx.trigger.severity}} - - Threat detector: {{ctx.detector.name}} - - Description: {{ctx.detector.description}} - - Detector data sources: {{ctx.detector.datasources}} - `.trim(), - MESSAGE_SUBJECT: `Triggered alert condition: {{ctx.trigger.name}} - Severity: {{ctx.trigger.severity}} - Threat detector: {{ctx.detector.name}} - `.trim(), -}; diff --git a/public/components/Notifications/NotificationForm.tsx b/public/components/Notifications/NotificationForm.tsx index 1cf520ec..059aa663 100644 --- a/public/components/Notifications/NotificationForm.tsx +++ b/public/components/Notifications/NotificationForm.tsx @@ -15,7 +15,8 @@ import { EuiSpacer, EuiCompressedSwitch, EuiText, - EuiCompressedTextArea, EuiCompressedCheckbox, + EuiCompressedTextArea, + EuiCompressedCheckbox, } from '@elastic/eui'; import React, { useState } from 'react'; import { NOTIFICATIONS_HREF } from '../../utils/constants'; @@ -24,6 +25,7 @@ import { NotificationChannelOption, NotificationChannelTypeOptions, TriggerAction, + TriggerContext, } from '../../../types'; import { getIsNotificationPluginInstalled } from '../../utils/helpers'; import Mustache from 'mustache'; @@ -38,7 +40,7 @@ export interface NotificationFormProps { onMessageBodyChange: (message: string) => void; onMessageSubjectChange: (subject: string) => void; onNotificationToggle?: (enabled: boolean) => void; - context: any + context: TriggerContext; } export const NotificationForm: React.FC = ({ @@ -68,12 +70,15 @@ export const NotificationForm: React.FC = ({ } let preview = ''; try { - preview = `${Mustache.render(action?.subject_template.source, context)}\n\n${Mustache.render(action?.message_template.source, context)}`; + preview = `${Mustache.render(action?.subject_template.source, context)}\n\n${Mustache.render( + action?.message_template.source, + context + )}`; } catch (err) { - preview = err.message; + preview = `There was an error rendering message preview: ${err.message}`; console.error('There was an error rendering mustache template', err); } - + ``; return ( <> = ({ onDisplayPreviewChange(e)} + onChange={onDisplayPreviewChange} /> {displayPreview ? ( - - - + + + + + ) : null} diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx index e5faa27a..0716de9f 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx @@ -30,9 +30,7 @@ import { NotificationChannelTypeOptions, } from '../../../../../../../types'; import { NotificationForm } from '../../../../../../components/Notifications/NotificationForm'; -import { ALERT_SEVERITY_OPTIONS } from '../../../../../../utils/constants'; -import { DEFAULT_MESSAGE_SOURCE } from '../../../../../../components/Commons/Constants'; -import Mustache from 'mustache'; +import { ALERT_SEVERITY_OPTIONS, DEFAULT_MESSAGE_SOURCE } from '../../../../../../utils/constants'; interface AlertConditionPanelProps extends RouteComponentProps { alertCondition: AlertCondition; @@ -110,14 +108,15 @@ export default class AlertConditionPanel extends Component< )}`; return { trigger: { - name:alertCondition.name, - severity: parseAlertSeverityToOption(alertCondition.severity)?.label || alertCondition.severity + name: alertCondition.name, + severity: + parseAlertSeverityToOption(alertCondition.severity)?.label || alertCondition.severity, }, detector: { name: detector.name, description: detectorInput.description, - datasources: detectorIndices - } + datasources: detectorIndices, + }, }; }; diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap index 387ce6c6..126c0d76 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap @@ -842,7 +842,7 @@ Object { >
@@ -1705,7 +1705,7 @@ Object { >
diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 25ab3da8..626c4d89 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -320,3 +320,16 @@ const LocalCluster: DataSourceOption = { export const dataSourceObservable = new BehaviorSubject({}); export const DATA_SOURCE_NOT_SET_ERROR = 'Data source is not set'; + +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export const DEFAULT_MESSAGE_SOURCE = { + MESSAGE_BODY: `- Triggered alert condition: {{ctx.trigger.name}} + - Severity: {{ctx.trigger.severity}} + - Threat detector: {{ctx.detector.name}} + - Description: {{ctx.detector.description}} + - Detector data sources: {{ctx.detector.datasources}}`, + MESSAGE_SUBJECT: `Triggered alert condition: {{ctx.trigger.name}} - Severity: {{ctx.trigger.severity}} - Threat detector: {{ctx.detector.name}}`, +}; diff --git a/types/Alert.ts b/types/Alert.ts index 5e600c57..3e34a46b 100644 --- a/types/Alert.ts +++ b/types/Alert.ts @@ -43,6 +43,18 @@ export interface TriggerAction { }; } +export interface TriggerContext { + trigger: { + name: string; + severity: string; + }; + detector: { + name: string; + description: string; + datasources: string; + }; +} + /** * API interfaces */ From 4da8746f6c835d1f6ed2e0fb9d0e526cd67c6775 Mon Sep 17 00:00:00 2001 From: Nishtha Mehrotra Date: Fri, 20 Dec 2024 16:08:04 -0800 Subject: [PATCH 4/4] Updated context type being passed to detector notification form --- .../Notifications/NotificationForm.tsx | 14 ++++++---- .../AlertCondition/AlertConditionPanel.tsx | 28 +++++++++---------- public/utils/constants.ts | 4 --- types/Alert.ts | 18 ++++++------ 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/public/components/Notifications/NotificationForm.tsx b/public/components/Notifications/NotificationForm.tsx index 059aa663..25a86021 100644 --- a/public/components/Notifications/NotificationForm.tsx +++ b/public/components/Notifications/NotificationForm.tsx @@ -18,7 +18,7 @@ import { EuiCompressedTextArea, EuiCompressedCheckbox, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { NOTIFICATIONS_HREF } from '../../utils/constants'; import { NotificationsCallOut } from '../NotificationsCallOut'; import { @@ -28,7 +28,7 @@ import { TriggerContext, } from '../../../types'; import { getIsNotificationPluginInstalled } from '../../utils/helpers'; -import Mustache from 'mustache'; +import { render } from 'mustache'; export interface NotificationFormProps { allNotificationChannels: NotificationChannelTypeOptions[]; @@ -58,8 +58,10 @@ export const NotificationForm: React.FC = ({ const hasNotificationPlugin = getIsNotificationPluginInstalled(); const [shouldSendNotification, setShouldSendNotification] = useState(!!action?.destination_id); const selectedNotificationChannelOption: NotificationChannelOption[] = []; - const onDisplayPreviewChange = (e) => setDisplayPreview(e.target.checked); const [displayPreview, setDisplayPreview] = useState(false); + const onDisplayPreviewChange = useCallback((e) => setDisplayPreview(e.target.checked), [ + displayPreview, + ]); if (shouldSendNotification && action?.destination_id) { allNotificationChannels.forEach((typeOption) => { const matchingChannel = typeOption.options.find( @@ -70,7 +72,7 @@ export const NotificationForm: React.FC = ({ } let preview = ''; try { - preview = `${Mustache.render(action?.subject_template.source, context)}\n\n${Mustache.render( + preview = `${render(action?.subject_template.source, context)}\n\n${render( action?.message_template.source, context )}`; @@ -194,7 +196,7 @@ export const NotificationForm: React.FC = ({ onChange={onDisplayPreviewChange} /> - {displayPreview ? ( + {displayPreview && ( = ({ /> - ) : null} + )} diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx index 0716de9f..f49c2fd4 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx @@ -28,6 +28,7 @@ import { Detector, NotificationChannelOption, NotificationChannelTypeOptions, + TriggerContext, } from '../../../../../../../types'; import { NotificationForm } from '../../../../../../components/Notifications/NotificationForm'; import { ALERT_SEVERITY_OPTIONS, DEFAULT_MESSAGE_SOURCE } from '../../../../../../utils/constants'; @@ -99,7 +100,7 @@ export default class AlertConditionPanel extends Component< }); } - getTriggerContext = () => { + getTriggerContext = (): TriggerContext => { const lineBreakAndTab = '\n\t'; const { alertCondition, detector } = this.props; const detectorInput = detector.inputs[0].detector_input; @@ -107,15 +108,17 @@ export default class AlertConditionPanel extends Component< `,${lineBreakAndTab}` )}`; return { - trigger: { - name: alertCondition.name, - severity: - parseAlertSeverityToOption(alertCondition.severity)?.label || alertCondition.severity, - }, - detector: { - name: detector.name, - description: detectorInput.description, - datasources: detectorIndices, + ctx: { + trigger: { + name: alertCondition.name, + severity: + parseAlertSeverityToOption(alertCondition.severity)?.label || alertCondition.severity, + }, + detector: { + name: detector.name, + description: detectorInput.description, + datasources: detectorIndices, + }, }, }; }; @@ -307,7 +310,6 @@ export default class AlertConditionPanel extends Component< }; render() { - const context = this.getTriggerContext(); const { alertCondition = getEmptyAlertCondition(), allNotificationChannels, @@ -559,9 +561,7 @@ export default class AlertConditionPanel extends Component< ({}); export const DATA_SOURCE_NOT_SET_ERROR = 'Data source is not set'; -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ export const DEFAULT_MESSAGE_SOURCE = { MESSAGE_BODY: `- Triggered alert condition: {{ctx.trigger.name}} - Severity: {{ctx.trigger.severity}} diff --git a/types/Alert.ts b/types/Alert.ts index 3e34a46b..c7b22a8b 100644 --- a/types/Alert.ts +++ b/types/Alert.ts @@ -44,14 +44,16 @@ export interface TriggerAction { } export interface TriggerContext { - trigger: { - name: string; - severity: string; - }; - detector: { - name: string; - description: string; - datasources: string; + ctx: { + trigger: { + name: string; + severity: string; + }; + detector: { + name: string; + description: string; + datasources: string; + }; }; }