Skip to content

Commit

Permalink
Updating Preview Message functionality while setting notifications in…
Browse files Browse the repository at this point in the history
… detector alerts (#1241)

* Updated Preview Message experience for notifications in detector trigger setup

* Updated detector notification template

* updated code review comments

* Updated context type being passed to detector notification form
  • Loading branch information
nishtham-amazon authored Dec 21, 2024
1 parent 251e695 commit a72fbc0
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 51 deletions.
48 changes: 38 additions & 10 deletions public/components/Notifications/NotificationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ import {
EuiCompressedSwitch,
EuiText,
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 {
NotificationChannelOption,
NotificationChannelTypeOptions,
TriggerAction,
TriggerContext,
} from '../../../types';
import { getIsNotificationPluginInstalled } from '../../utils/helpers';
import { render } from 'mustache';

export interface NotificationFormProps {
allNotificationChannels: NotificationChannelTypeOptions[];
Expand All @@ -37,10 +40,12 @@ export interface NotificationFormProps {
onMessageBodyChange: (message: string) => void;
onMessageSubjectChange: (subject: string) => void;
onNotificationToggle?: (enabled: boolean) => void;
context: TriggerContext;
}

export const NotificationForm: React.FC<NotificationFormProps> = ({
action,
context,
allNotificationChannels,
loadingNotifications,
prepareMessage,
Expand All @@ -53,6 +58,10 @@ export const NotificationForm: React.FC<NotificationFormProps> = ({
const hasNotificationPlugin = getIsNotificationPluginInstalled();
const [shouldSendNotification, setShouldSendNotification] = useState(!!action?.destination_id);
const selectedNotificationChannelOption: NotificationChannelOption[] = [];
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(
Expand All @@ -61,7 +70,17 @@ export const NotificationForm: React.FC<NotificationFormProps> = ({
if (matchingChannel) selectedNotificationChannelOption.push(matchingChannel);
});
}

let preview = '';
try {
preview = `${render(action?.subject_template.source, context)}\n\n${render(
action?.message_template.source,
context
)}`;
} catch (err) {
preview = `There was an error rendering message preview: ${err.message}`;
console.error('There was an error rendering mustache template', err);
}
``;
return (
<>
<EuiCompressedSwitch
Expand Down Expand Up @@ -168,15 +187,24 @@ export const NotificationForm: React.FC<NotificationFormProps> = ({
/>
</EuiCompressedFormRow>
</EuiFlexItem>
{prepareMessage && (

<EuiFlexItem>
<EuiCompressedCheckbox
id="notification-message-preview-checkbox"
label="Preview message"
checked={displayPreview}
onChange={onDisplayPreviewChange}
/>
</EuiFlexItem>
{displayPreview && (
<EuiFlexItem>
<EuiCompressedFormRow>
<EuiSmallButton
fullWidth={false}
onClick={() => prepareMessage(true /* updateMessage */)}
>
Generate message
</EuiSmallButton>
<EuiCompressedFormRow label="Message preview" fullWidth>
<EuiCompressedTextArea
placeholder="Preview of mustache template"
fullWidth
value={preview}
readOnly
/>
</EuiCompressedFormRow>
</EuiFlexItem>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ import {
Detector,
NotificationChannelOption,
NotificationChannelTypeOptions,
TriggerContext,
} from '../../../../../../../types';
import { NotificationForm } from '../../../../../../components/Notifications/NotificationForm';
import { ALERT_SEVERITY_OPTIONS } from '../../../../../../utils/constants';
import { ALERT_SEVERITY_OPTIONS, DEFAULT_MESSAGE_SOURCE } from '../../../../../../utils/constants';

interface AlertConditionPanelProps extends RouteComponentProps {
alertCondition: AlertCondition;
Expand Down Expand Up @@ -99,6 +100,29 @@ export default class AlertConditionPanel extends Component<
});
}

getTriggerContext = (): TriggerContext => {
const lineBreakAndTab = '\n\t';
const { alertCondition, detector } = this.props;
const detectorInput = detector.inputs[0].detector_input;
const detectorIndices = `${lineBreakAndTab}${detectorInput.indices.join(
`,${lineBreakAndTab}`
)}`;
return {
ctx: {
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.
Expand All @@ -113,7 +137,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 = DEFAULT_MESSAGE_SOURCE.MESSAGE_SUBJECT;

if (updateMessage || !alertCondition.actions[0]?.subject_template.source)
this.onMessageSubjectChange(defaultSubject, !onMount);
Expand Down Expand Up @@ -157,7 +181,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);
}
};

Expand Down Expand Up @@ -537,6 +561,7 @@ export default class AlertConditionPanel extends Component<

<NotificationForm
action={alertCondition.actions[0]}
context={this.getTriggerContext()}
allNotificationChannels={allNotificationChannels}
loadingNotifications={loadingNotifications}
prepareMessage={this.prepareMessage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -838,28 +838,22 @@ Object {
class="euiFlexItem"
>
<div
class="euiFormRow euiFormRow--compressed"
id="some_html_id-row"
class="euiCheckbox euiCheckbox--compressed"
>
<input
class="euiCheckbox__input"
id="notification-message-preview-checkbox"
type="checkbox"
/>
<div
class="euiFormRow__fieldWrapper"
class="euiCheckbox__square"
/>
<label
class="euiCheckbox__label"
for="notification-message-preview-checkbox"
>
<button
class="euiButton euiButton--primary euiButton--small"
id="some_html_id"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Generate message
</span>
</span>
</button>
</div>
Preview message
</label>
</div>
</div>
</div>
Expand Down Expand Up @@ -1707,28 +1701,22 @@ Object {
class="euiFlexItem"
>
<div
class="euiFormRow euiFormRow--compressed"
id="some_html_id-row"
class="euiCheckbox euiCheckbox--compressed"
>
<input
class="euiCheckbox__input"
id="notification-message-preview-checkbox"
type="checkbox"
/>
<div
class="euiFormRow__fieldWrapper"
class="euiCheckbox__square"
/>
<label
class="euiCheckbox__label"
for="notification-message-preview-checkbox"
>
<button
class="euiButton euiButton--primary euiButton--small"
id="some_html_id"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Generate message
</span>
</span>
</button>
</div>
Preview message
</label>
</div>
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions public/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,12 @@ const LocalCluster: DataSourceOption = {
export const dataSourceObservable = new BehaviorSubject<DataSourceOption>({});

export const DATA_SOURCE_NOT_SET_ERROR = 'Data source is not set';

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}}`,
};
14 changes: 14 additions & 0 deletions types/Alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ export interface TriggerAction {
};
}

export interface TriggerContext {
ctx: {
trigger: {
name: string;
severity: string;
};
detector: {
name: string;
description: string;
datasources: string;
};
};
}

/**
* API interfaces
*/
Expand Down

0 comments on commit a72fbc0

Please sign in to comment.