Skip to content

Commit

Permalink
Alert redesign #4 - custom notification template (#4170)
Browse files Browse the repository at this point in the history
  • Loading branch information
ranbena committed Oct 5, 2019
1 parent bc6697a commit 7740deb
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 76 deletions.
4 changes: 4 additions & 0 deletions client/app/components/HelpTrigger.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export const TYPES = {
'/open-source/setup/#Mail-Configuration',
'Guide: Mail Configuration',
],
ALERT_NOTIF_TEMPLATE_GUIDE: [
'/user-guide/alerts/custom-alert-notifications',
'Guide: Custom Alerts Notifications',
],
};

export class HelpTrigger extends React.Component {
Expand Down
35 changes: 35 additions & 0 deletions client/app/components/proptypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,45 @@ export const Destination = PropTypes.shape({
type: PropTypes.string.isRequired,
});

export const Query = PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
description: PropTypes.string,
data_source_id: PropTypes.number.isRequired,
created_at: PropTypes.string.isRequired,
updated_at: PropTypes.string,
user: UserProfile,
query: PropTypes.string,
queryHash: PropTypes.string,
is_safe: PropTypes.bool.isRequired,
is_draft: PropTypes.bool.isRequired,
is_archived: PropTypes.bool.isRequired,
api_key: PropTypes.string.isRequired,
});

export const AlertOptions = PropTypes.shape({
column: PropTypes.string,
op: PropTypes.oneOf(['greater than', 'less than', 'equals']),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
custom_subject: PropTypes.string,
custom_body: PropTypes.string,
});

export const Alert = PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
created_at: PropTypes.string,
last_triggered_at: PropTypes.string,
updated_at: PropTypes.string,
rearm: PropTypes.number,
state: PropTypes.oneOf(['ok', 'triggered', 'unknown']),
user: UserProfile,
query: Query.isRequired,
options: PropTypes.shape({
column: PropTypes.string,
op: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}).isRequired,
});

function checkMoment(isRequired, props, propName, componentName) {
Expand Down
15 changes: 15 additions & 0 deletions client/app/pages/alert/Alert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Dropdown from 'antd/lib/dropdown';
import Menu from 'antd/lib/menu';

import Criteria from './components/Criteria';
import NotificationTemplate from './components/NotificationTemplate';
import Rearm from './components/Rearm';
import Query from './components/Query';
import AlertDestinations from './components/AlertDestinations';
Expand Down Expand Up @@ -349,10 +350,24 @@ class AlertPage extends React.Component {
<HorizontalFormItem label="When triggered, send notification">
<Rearm value={pendingRearm || 0} onChange={this.onRearmChange} editMode />
</HorizontalFormItem>
<HorizontalFormItem label="Template">
<NotificationTemplate
alert={alert}
query={query}
columnNames={queryResult.getColumnNames()}
resultValues={queryResult.getData()}
subject={options.custom_subject}
setSubject={subject => this.setAlertOptions({ custom_subject: subject })}
body={options.custom_body}
setBody={body => this.setAlertOptions({ custom_body: body })}
/>
</HorizontalFormItem>
</>
) : (
<HorizontalFormItem label="Notifications" className="form-item-line-height-normal">
<Rearm value={pendingRearm || 0} />
<br />
Set to {options.custom_subject || options.custom_body ? 'custom' : 'default'} notification template.
</HorizontalFormItem>
)}
</>
Expand Down
122 changes: 122 additions & 0 deletions client/app/pages/alert/components/NotificationTemplate.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { head } from 'lodash';
import Mustache from 'mustache';

import { HelpTrigger } from '@/components/HelpTrigger';
import { Alert as AlertType, Query as QueryType } from '@/components/proptypes';

import Input from 'antd/lib/input';
import Select from 'antd/lib/select';
import Modal from 'antd/lib/modal';
import Switch from 'antd/lib/switch';

import './NotificationTemplate.less';


function normalizeCustomTemplateData(alert, query, columnNames, resultValues) {
const topValue = resultValues && head(resultValues)[alert.options.column];

return {
ALERT_STATUS: 'TRIGGERED',
ALERT_CONDITION: alert.options.op,
ALERT_THRESHOLD: alert.options.value,
ALERT_NAME: alert.name,
ALERT_URL: `${window.location.origin}/alerts/${alert.id}`,
QUERY_NAME: query.name,
QUERY_URL: `${window.location.origin}/queries/${query.id}`,
QUERY_RESULT_VALUE: topValue,
QUERY_RESULT_ROWS: resultValues,
QUERY_RESULT_COLS: columnNames,
};
}

function NotificationTemplate({ alert, query, columnNames, resultValues, subject, setSubject, body, setBody }) {
const hasContent = !!(subject || body);
const [enabled, setEnabled] = useState(hasContent ? 1 : 0);
const [showPreview, setShowPreview] = useState(false);

const renderData = normalizeCustomTemplateData(alert, query, columnNames, resultValues);

const render = tmpl => Mustache.render(tmpl || '', renderData);
const onEnabledChange = (value) => {
if (value || !hasContent) {
setEnabled(value);
setShowPreview(false);
} else {
Modal.confirm({
title: 'Are you sure?',
content: 'Switching to default template will discard your custom template.',
onOk: () => {
setSubject(null);
setBody(null);
setEnabled(value);
setShowPreview(false);
},
maskClosable: true,
autoFocusButton: null,
});
}
};

return (
<div className="alert-template">
<Select
value={enabled}
onChange={onEnabledChange}
optionLabelProp="label"
dropdownMatchSelectWidth={false}
style={{ width: 'fit-content' }}
>
<Select.Option value={0} label="Use default template">
Default template
</Select.Option>
<Select.Option value={1} label="Use custom template">
Custom template
</Select.Option>
</Select>
{!!enabled && (
<div className="alert-custom-template" data-test="AlertCustomTemplate">
<div className="d-flex align-items-center">
<h5 className="flex-fill">Subject / Body</h5>
Preview <Switch size="small" className="alert-template-preview" value={showPreview} onChange={setShowPreview} />
</div>
<Input
value={showPreview ? render(subject) : subject}
onChange={e => setSubject(e.target.value)}
disabled={showPreview}
data-test="CustomSubject"
/>
<Input.TextArea
value={showPreview ? render(body) : body}
autosize={{ minRows: 9 }}
onChange={e => setBody(e.target.value)}
disabled={showPreview}
data-test="CustomBody"
/>
<HelpTrigger type="ALERT_NOTIF_TEMPLATE_GUIDE" className="f-13">
<i className="fa fa-question-circle" /> Formatting guide
</HelpTrigger>
</div>
)}
</div>
);
}

NotificationTemplate.propTypes = {
alert: AlertType.isRequired,
query: QueryType.isRequired,
columnNames: PropTypes.arrayOf(PropTypes.string).isRequired,
resultValues: PropTypes.arrayOf(PropTypes.any).isRequired,
subject: PropTypes.string,
setSubject: PropTypes.func.isRequired,
body: PropTypes.string,
setBody: PropTypes.func.isRequired,
};

NotificationTemplate.defaultProps = {
subject: '',
body: '',
};

export default NotificationTemplate;
36 changes: 36 additions & 0 deletions client/app/pages/alert/components/NotificationTemplate.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.alert-template {
display: flex;
flex-direction: column;

input {
margin-bottom: 10px;
}

textarea {
margin-bottom: 0 !important;
}

input, textarea {
font-family: "Roboto Mono", monospace;
font-size: 12px;
letter-spacing: -0.4px ;

&[disabled] {
color: inherit;
cursor: auto;
}
}

.alert-custom-template {
margin-top: 10px;
padding: 4px 10px 2px;
background: #fbfbfb;
border: 1px dashed #d9d9d9;
border-radius: 3px;
max-width: 500px;
}

.alert-template-preview {
margin: 0 0 0 5px !important;
}
}
41 changes: 0 additions & 41 deletions client/app/services/alert-template.js

This file was deleted.

28 changes: 28 additions & 0 deletions client/cypress/integration/alert/edit_alert_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,32 @@ describe('Edit Alert', () => {
cy.percySnapshot('Edit Alert screen');
});
});

it('edits the notification template and takes a screenshot', () => {
createQuery()
.then(({ id: queryId }) => createAlert(queryId, { custom_subject: 'FOO', custom_body: 'BAR' }))
.then(({ id: alertId }) => {
cy.visit(`/alerts/${alertId}/edit`);
cy.getByTestId('AlertCustomTemplate').should('exist');
cy.percySnapshot('Alert Custom Template screen');
});
});

it('previews rendered template correctly', () => {
const options = {
value: '123',
op: 'equals',
custom_subject: '{{ ALERT_CONDITION }}',
custom_body: '{{ ALERT_THRESHOLD }}',
};

createQuery()
.then(({ id: queryId }) => createAlert(queryId, options))
.then(({ id: alertId }) => {
cy.visit(`/alerts/${alertId}/edit`);
cy.get('.alert-template-preview').click();
cy.getByTestId('CustomSubject').should('have.value', options.op);
cy.getByTestId('CustomBody').should('have.value', options.value);
});
});
});
15 changes: 8 additions & 7 deletions redash/destinations/chatwork.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,21 @@ def notify(self, alert, query, user, new_state, app, host, options):
# Documentation: http://developer.chatwork.com/ja/endpoint_rooms.html#POST-rooms-room_id-messages
url = 'https://api.chatwork.com/v2/rooms/{room_id}/messages'.format(room_id=options.get('room_id'))

alert_url = '{host}/alerts/{alert_id}'.format(host=host, alert_id=alert.id)
query_url = '{host}/queries/{query_id}'.format(host=host, query_id=query.id)
message_template = options.get('message_template', ChatWork.ALERTS_DEFAULT_MESSAGE_TEMPLATE)
message = ''
if alert.custom_subject:
message = alert.custom_subject + '\n'
message += message_template.replace('\\n', '\n').format(

if alert.custom_body:
message += alert.custom_body
else:
alert_url = '{host}/alerts/{alert_id}'.format(host=host, alert_id=alert.id)
query_url = '{host}/queries/{query_id}'.format(host=host, query_id=query.id)
message_template = options.get('message_template', ChatWork.ALERTS_DEFAULT_MESSAGE_TEMPLATE)
message += message_template.replace('\\n', '\n').format(
alert_name=alert.name, new_state=new_state.upper(),
alert_url=alert_url,
query_url=query_url)

if alert.template:
description = alert.render_template()
message = message + "\n" + description
headers = {'X-ChatWorkToken': options.get('api_token')}
payload = {'body': message}

Expand Down
12 changes: 6 additions & 6 deletions redash/destinations/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ def notify(self, alert, query, user, new_state, app, host, options):
if not recipients:
logging.warning("No emails given. Skipping send.")

html = """
Check <a href="{host}/alerts/{alert_id}">alert</a> / check <a href="{host}/queries/{query_id}">query</a> </br>.
""".format(host=host, alert_id=alert.id, query_id=query.id)
if alert.template:
description = alert.render_template()
html += "<br>" + description
if alert.custom_body:
html = alert.custom_body
else:
html = """
Check <a href="{host}/alerts/{alert_id}">alert</a> / check <a href="{host}/queries/{query_id}">query</a> </br>.
""".format(host=host, alert_id=alert.id, query_id=query.id)
logging.debug("Notifying: %s", recipients)

try:
Expand Down
4 changes: 2 additions & 2 deletions redash/destinations/hangoutschat.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ def notify(self, alert, query, user, new_state, app, host, options):
]
}

if alert.template:
if alert.custom_body:
data["cards"][0]["sections"].append({
"widgets": [
{
"textParagraph": {
"text": alert.render_template()
"text": alert.custom_body
}
}
]
Expand Down
Loading

0 comments on commit 7740deb

Please sign in to comment.