Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build custom alert message #3137

Merged
merged 25 commits into from
Jul 11, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions client/app/pages/alert/alert.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,35 @@
</div>
</div>
</div>
<div ng-show="$ctrl.selectedQuery">
<div class="form-group" ng-show="$ctrl.selectedQuery">
<label>Description Template</label>
<i class="fa fa-question-circle" uib-tooltip="{{$ctrl.templateHelpMsg}}"></i>
<div class="row bg-white p-b-5" ng-if="$ctrl.canEdit" resizable r-directions="['bottom']" r-height="300" style="min-height:100px;">
<div ui-ace="$ctrl.editorOptions" ng-model="$ctrl.alert.template"></div>
</div>
</div>
<div class="form-group" ng-if="$ctrl.canEdit">
<button class="btn btn-default" ng-click="$ctrl.preview()">Preview</button>
<label for="show-as-html">Show As HTML</label>
<input type="checkbox" name="show-as-html" ng-model="$ctrl.showAsHTML">
</div>
<div class="panel panel-default" ng-if="$ctrl.alert.preview">
<div class="panel-heading">
<label for="hide-preview">Hide Preview</label>
<input type="checkbox" name="hide-preview" ng-model="$ctrl.hidePreview">
</div>
<div class="panel-body" ng-if="$ctrl.hidePreview == false">
<div ng-if="!$ctrl.showAsHTML">
<div ng-bind-html="$ctrl.alert.previewHTML"></div>
</div>
<div ng-if="$ctrl.showAsHTML">
<div ng-bind-html="$ctrl.alert.preview"></div>
</div>
</div>
<div class="panel-footer"></div>
</div>
</div>

<div class="form-group" ng-if="$ctrl.canEdit">
<button class="btn btn-primary" ng-disabled="!alertForm.$valid" ng-click="$ctrl.saveChanges()">Save</button>
Expand Down
46 changes: 45 additions & 1 deletion client/app/pages/alert/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import { template as templateBuilder } from 'lodash';
import template from './alert.html';

function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Events, Alert) {
function AlertCtrl($routeParams, $location, $sce, $http, toastr, currentUser, Query, Events, Alert) {
k-tomoyasu marked this conversation as resolved.
Show resolved Hide resolved
this.alertId = $routeParams.alertId;
this.hidePreview = false;
this.templateHelpMsg = `using template engine "Jinja2".
you can build message with latest query result.
variable name "rows" is assigned as result rows. "cols" as result columns.`;
this.editorOptions = {
useWrapMode: true,
showPrintMargin: false,
advanced: {
behavioursEnabled: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
autoScrollEditorIntoView: true,
},
onLoad(editor) {
editor.$blockScrolling = Infinity;
},
};

this.preview = () => {
const result = this.queryResult.query_result.data;
const url = 'api/alerts/template';
$http
.post(url, { template: this.alert.template, data: result })
.success((res) => {
const data = JSON.parse(res);
const preview = data.preview;
this.alert.preview = $sce.trustAsHtml(preview);
const replaced = preview
.replace(/"/g, '&quot;')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
this.alert.previewHTML = $sce.trustAsHtml(replaced.replace(/\n|\r/g, '<br>'));
if (data.error) {
toastr.error('Unable to build description. please confirm your template.', { timeOut: 10000 });
}
})
.error(() => {
toastr.error('Failed. unexpected error.');
});
};

if (this.alertId === 'new') {
Events.record('view', 'page', 'alerts/new');
Expand Down Expand Up @@ -57,6 +98,9 @@ function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Ev
if (this.alert.rearm === '' || this.alert.rearm === 0) {
this.alert.rearm = null;
}
if (this.alert.template === undefined || this.alert.template === '') {
this.alert.template = null;
}
this.alert.$save(
(alert) => {
toastr.success('Saved.');
Expand Down
28 changes: 28 additions & 0 deletions migrations/versions/ed7bf6adbd4d_add_alert_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""add_alert_template_column

Revision ID: ed7bf6adbd4d
Revises: 71477dadd6ef
Create Date: 2018-11-28 22:56:40.494028

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'ed7bf6adbd4d'
down_revision = '73beceabb948'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('alerts', sa.Column('template', sa.Text(), nullable=True))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put template in the options JSON object we already have in alerts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, fix logic and remove migration file.

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('alerts', 'template')
# ### end Alembic commands ###
6 changes: 5 additions & 1 deletion redash/destinations/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ def notify(self, alert, query, user, new_state, app, host, options):
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>.
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
logging.debug("Notifying: %s", recipients)

try:
Expand All @@ -52,4 +55,5 @@ def notify(self, alert, query, user, new_state, app, host, options):
except Exception:
logging.exception("Mail send error.")


register(Email)
6 changes: 6 additions & 0 deletions redash/destinations/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ def notify(self, alert, query, user, new_state, app, host, options):
if new_state == "triggered":
text = alert.name + " just triggered"
color = "#c0392b"
if alert.template:
description, _ = alert.render_template(True)
fields.append({
"title": "Description",
"value": description
})
else:
text = alert.name + " went back to normal"
color = "#27ae60"
Expand Down
3 changes: 2 additions & 1 deletion redash/destinations/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ def notify(self, alert, query, user, new_state, app, host, options):
data = {
'event': 'alert_state_change',
'alert': serialize_alert(alert, full=False),
'url_base': host
'url_base': host,
"description": alert.render_template(True) if alert.template else ''
}
headers = {'Content-Type': 'application/json'}
auth = HTTPBasicAuth(options.get('username'), options.get('password')) if options.get('username') else None
Expand Down
18 changes: 16 additions & 2 deletions redash/handlers/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_fields)
from redash.permissions import (require_access, require_admin_or_owner,
require_permission, view_only)
from redash.utils import json_dumps, render_custom_template


class AlertResource(BaseResource):
Expand All @@ -24,7 +25,7 @@ def get(self, alert_id):

def post(self, alert_id):
req = request.get_json(True)
params = project(req, ('options', 'name', 'query_id', 'rearm'))
params = project(req, ('options', 'name', 'query_id', 'rearm', 'template'))
alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org)
require_admin_or_owner(alert.user.id)

Expand Down Expand Up @@ -60,7 +61,8 @@ def post(self):
query_rel=query,
user=self.current_user,
rearm=req.get('rearm'),
options=req['options']
options=req['options'],
template=req['template']
)

models.db.session.add(alert)
Expand Down Expand Up @@ -131,3 +133,15 @@ def delete(self, alert_id, subscriber_id):
'object_id': alert_id,
'object_type': 'alert'
})


class AlertTemplateResource(BaseResource):
def post(self):
req = request.get_json(True)
data = req.get("data", "")
if 'rows' not in data or 'columns' not in data:
return json_dumps({'preview': 'no query result.', "error": True})

template = req.get("template", "")
preview, err = render_custom_template(template, data['rows'], data['columns'], True)
return json_dumps({'preview': preview, "error": err})
5 changes: 3 additions & 2 deletions redash/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from redash.utils import json_dumps
from redash.handlers.base import org_scoped_rule
from redash.handlers.permissions import ObjectPermissionsListResource, CheckPermissionResource
from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource
from redash.handlers.dashboards import DashboardListResource, DashboardResource, DashboardShareResource, PublicDashboardResource
from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource, AlertTemplateResource
from redash.handlers.dashboards import DashboardListResource, DashboardResource, DashboardShareResource, PublicDashboardResource
k-tomoyasu marked this conversation as resolved.
Show resolved Hide resolved
from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource
from redash.handlers.events import EventsResource
from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource
Expand Down Expand Up @@ -44,6 +44,7 @@ def json_representation(data, code, headers=None):


api.add_org_resource(AlertResource, '/api/alerts/<alert_id>', endpoint='alert')
api.add_org_resource(AlertTemplateResource, '/api/alerts/template', endpoint='alert_template')
api.add_org_resource(AlertSubscriptionListResource, '/api/alerts/<alert_id>/subscriptions', endpoint='alert_subscriptions')
api.add_org_resource(AlertSubscriptionResource, '/api/alerts/<alert_id>/subscriptions/<subscriber_id>', endpoint='alert_subscription')
api.add_org_resource(AlertListResource, '/api/alerts', endpoint='alerts')
Expand Down
7 changes: 6 additions & 1 deletion redash/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from redash.metrics import database # noqa: F401
from redash.query_runner import (get_configuration_schema_for_query_runner_type,
get_query_runner)
from redash.utils import generate_token, json_dumps, json_loads
from redash.utils import generate_token, json_dumps, json_loads, render_custom_template
from redash.utils.configuration import ConfigurationContainer

from .base import db, gfk_type, Column, GFKBase, SearchBaseQuery
Expand Down Expand Up @@ -720,6 +720,7 @@ class Alert(TimestampMixin, BelongsToOrgMixin, db.Model):
subscriptions = db.relationship("AlertSubscription", cascade="all, delete-orphan")
last_triggered_at = Column(db.DateTime(True), nullable=True)
rearm = Column(db.Integer, nullable=True)
template = Column(db.Text, nullable=True)

__tablename__ = 'alerts'

Expand Down Expand Up @@ -766,6 +767,10 @@ def evaluate(self):
def subscribers(self):
return User.query.join(AlertSubscription).filter(AlertSubscription.alert == self)

def render_template(self, showError=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showError seems to be unused? Also should be show_error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! I've removed.

data = json_loads(self.query_rel.latest_query_data.data)
return render_custom_template(self.template, data['rows'], data['columns'])

@property
def groups(self):
return self.query_rel.groups
Expand Down
3 changes: 2 additions & 1 deletion redash/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ def serialize_alert(alert, full=True):
'last_triggered_at': alert.last_triggered_at,
'updated_at': alert.updated_at,
'created_at': alert.created_at,
'rearm': alert.rearm
'rearm': alert.rearm,
'template': alert.template
}

if full:
Expand Down
17 changes: 17 additions & 0 deletions redash/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from funcy import select_values
from redash import settings
from sqlalchemy.orm.query import Query
from jinja2 import Template, Environment

from .human_time import parse_human_time

Expand Down Expand Up @@ -131,6 +132,22 @@ def build_url(request, host, path):
return "{}://{}{}".format(request.scheme, host, path)


def render_custom_template(template, rows, columns, showError=None):
try:
renderer = Template(template)
message = renderer.render(rows=rows, cols=columns)
err = False
return message, err
except Exception as e:
err = True
if showError is None:
message = "Can not build description. Please confirm it's template."
return message, err
else:
message = e.message
return message, err


class UnicodeWriter:
"""
A CSV writer which will write rows to CSV file "f",
Expand Down
3 changes: 2 additions & 1 deletion tests/handlers/test_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ def test_returns_200_if_has_access_to_query(self):
db.session.commit()
rv = self.make_request('post', "/api/alerts", data=dict(name='Alert', query_id=query.id,
destination_id=destination.id, options={},
rearm=100))
rearm=100, template="alert-template"))
self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.json['rearm'], 100)
self.assertEqual(rv.json['template'], "alert-template")

def test_fails_if_doesnt_have_access_to_query(self):
data_source = self.factory.create_data_source(group=self.factory.create_group())
Expand Down