From efa10d86e537591aa03b33d913c85148bdf77386 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Wed, 28 Nov 2018 23:50:28 +0900 Subject: [PATCH 01/19] build custom alert message --- client/app/pages/alert/alert.html | 29 ++++++++++++++++++ client/app/pages/alert/index.js | 46 +++++++++++++++++++++++++++- migrations/versions/ed7bf6adbd4d_.py | 28 +++++++++++++++++ redash/destinations/email.py | 5 ++- redash/destinations/slack.py | 6 ++++ redash/handlers/alerts.py | 15 +++++++-- redash/handlers/api.py | 3 +- redash/models.py | 7 ++++- redash/serializers.py | 3 +- redash/utils/__init__.py | 16 ++++++++++ 10 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 migrations/versions/ed7bf6adbd4d_.py diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html index 737f384624..56992e4695 100644 --- a/client/app/pages/alert/alert.html +++ b/client/app/pages/alert/alert.html @@ -56,6 +56,35 @@ +
+
+ + +
+
+
+
+
+ + + +
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js index e5548f77ef..cda48429d1 100644 --- a/client/app/pages/alert/index.js +++ b/client/app/pages/alert/index.js @@ -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) { 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 = { + advanced: { + behavioursEnabled: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + autoScrollEditorIntoView: true, + }, + onLoad(editor) { + editor.$blockScrolling = Infinity; + editor.getSession().setUseWrapMode(true); + editor.setShowPrintMargin(false); + }, + }; + + 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, '"') + .replace(/&/g, '&') + .replace(//g, '>'); + this.alert.previewHTML = $sce.trustAsHtml(replaced.replace(/\n|\r/g, '
')); + 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'); @@ -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.'); diff --git a/migrations/versions/ed7bf6adbd4d_.py b/migrations/versions/ed7bf6adbd4d_.py new file mode 100644 index 0000000000..629bbee9ac --- /dev/null +++ b/migrations/versions/ed7bf6adbd4d_.py @@ -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 = '71477dadd6ef' +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)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('alerts', 'template') + # ### end Alembic commands ### diff --git a/redash/destinations/email.py b/redash/destinations/email.py index b66c9e9484..4774a0bd03 100644 --- a/redash/destinations/email.py +++ b/redash/destinations/email.py @@ -35,8 +35,11 @@ def notify(self, alert, query, user, new_state, app, host, options): logging.warning("No emails given. Skipping send.") html = """ - Check alert / check query. + Check alert / check query
. """.format(host=host, alert_id=alert.id, query_id=query.id) + if alert.template: + description, _ = alert.render_template() + html += "
" + description logging.debug("Notifying: %s", recipients) try: diff --git a/redash/destinations/slack.py b/redash/destinations/slack.py index e9e90ebf9c..1740a76465 100644 --- a/redash/destinations/slack.py +++ b/redash/destinations/slack.py @@ -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" diff --git a/redash/handlers/alerts.py b/redash/handlers/alerts.py index cc6bc9f1f1..faa3f356b4 100644 --- a/redash/handlers/alerts.py +++ b/redash/handlers/alerts.py @@ -9,7 +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): def get(self, alert_id): @@ -24,7 +24,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) @@ -60,7 +60,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) @@ -131,3 +132,11 @@ 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) + template = req.get("template", "") + data = req.get("data", "") + preview, err = render_custom_template(template, data['rows'], data['columns'], True) + return json_dumps({'preview': preview, "error": err }) diff --git a/redash/handlers/api.py b/redash/handlers/api.py index f8ef199857..23f427e970 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -5,7 +5,7 @@ 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.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource, AlertTemplateResource from redash.handlers.dashboards import DashboardListResource, DashboardResource, DashboardShareResource, PublicDashboardResource from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource from redash.handlers.events import EventsResource @@ -44,6 +44,7 @@ def json_representation(data, code, headers=None): api.add_org_resource(AlertResource, '/api/alerts/', endpoint='alert') +api.add_org_resource(AlertTemplateResource, '/api/alerts/template', endpoint='alert_template') api.add_org_resource(AlertSubscriptionListResource, '/api/alerts//subscriptions', endpoint='alert_subscriptions') api.add_org_resource(AlertSubscriptionResource, '/api/alerts//subscriptions/', endpoint='alert_subscription') api.add_org_resource(AlertListResource, '/api/alerts', endpoint='alerts') diff --git a/redash/models.py b/redash/models.py index 4c7327efbf..c080a1637a 100644 --- a/redash/models.py +++ b/redash/models.py @@ -21,7 +21,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 redash.settings.organization import settings as org_settings @@ -1288,6 +1288,7 @@ class Alert(TimestampMixin, 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' @@ -1325,6 +1326,10 @@ def evaluate(self): def subscribers(self): return User.query.join(AlertSubscription).filter(AlertSubscription.alert == self) + def render_description(self, showError=None): + 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 diff --git a/redash/serializers.py b/redash/serializers.py index d809a1f73e..9a059699cd 100644 --- a/redash/serializers.py +++ b/redash/serializers.py @@ -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: diff --git a/redash/utils/__init__.py b/redash/utils/__init__.py index fdca1680f4..2b2279bb4b 100644 --- a/redash/utils/__init__.py +++ b/redash/utils/__init__.py @@ -15,6 +15,7 @@ from funcy import distinct, select_values from six import string_types from sqlalchemy.orm.query import Query +from jinja2 import Template, Environment from .human_time import parse_human_time from redash import settings @@ -112,6 +113,21 @@ 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: """ From 82b8cf73679ff69e440a9433d4e0ef185163c237 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Sat, 1 Dec 2018 18:25:36 +0900 Subject: [PATCH 02/19] fit button color tone --- client/app/pages/alert/alert.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html index 56992e4695..43885db602 100644 --- a/client/app/pages/alert/alert.html +++ b/client/app/pages/alert/alert.html @@ -65,7 +65,7 @@
- +
From fdd6937741420267a6e6b5bc7908888cc8d59d69 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Sat, 1 Dec 2018 18:56:55 +0900 Subject: [PATCH 03/19] pass existing test --- tests/handlers/test_alerts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/handlers/test_alerts.py b/tests/handlers/test_alerts.py index 638595cb0d..a36435601b 100644 --- a/tests/handlers/test_alerts.py +++ b/tests/handlers/test_alerts.py @@ -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()) From 0908c0b4b357f54b427d0a03a85945d78d7a93f7 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Sat, 1 Dec 2018 20:32:42 +0900 Subject: [PATCH 04/19] fix typos --- redash/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redash/models.py b/redash/models.py index c080a1637a..bfb380775c 100644 --- a/redash/models.py +++ b/redash/models.py @@ -1326,8 +1326,8 @@ def evaluate(self): def subscribers(self): return User.query.join(AlertSubscription).filter(AlertSubscription.alert == self) - def render_description(self, showError=None): - data = json.loads(self.query_rel.latest_query_data.data) + def render_template(self, showError=None): + data = json_loads(self.query_rel.latest_query_data.data) return render_custom_template(self.template, data['rows'], data['columns']) @property From a7f607655afc433078e26a1754b9fe81afafd298 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Sat, 1 Dec 2018 20:33:08 +0900 Subject: [PATCH 05/19] follow code style --- client/app/pages/alert/index.js | 4 ++-- redash/destinations/email.py | 3 ++- redash/handlers/alerts.py | 4 +++- redash/utils/__init__.py | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js index cda48429d1..f667237dc4 100644 --- a/client/app/pages/alert/index.js +++ b/client/app/pages/alert/index.js @@ -8,6 +8,8 @@ function AlertCtrl($routeParams, $location, $sce, $http, toastr, currentUser, Qu 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, @@ -16,8 +18,6 @@ function AlertCtrl($routeParams, $location, $sce, $http, toastr, currentUser, Qu }, onLoad(editor) { editor.$blockScrolling = Infinity; - editor.getSession().setUseWrapMode(true); - editor.setShowPrintMargin(false); }, }; diff --git a/redash/destinations/email.py b/redash/destinations/email.py index 4774a0bd03..fa804542e1 100644 --- a/redash/destinations/email.py +++ b/redash/destinations/email.py @@ -38,7 +38,7 @@ def notify(self, alert, query, user, new_state, app, host, options): Check alert / check query
. """.format(host=host, alert_id=alert.id, query_id=query.id) if alert.template: - description, _ = alert.render_template() + description, _ = alert.render_template() html += "
" + description logging.debug("Notifying: %s", recipients) @@ -55,4 +55,5 @@ def notify(self, alert, query, user, new_state, app, host, options): except Exception: logging.exception("Mail send error.") + register(Email) diff --git a/redash/handlers/alerts.py b/redash/handlers/alerts.py index faa3f356b4..1c5917b688 100644 --- a/redash/handlers/alerts.py +++ b/redash/handlers/alerts.py @@ -11,6 +11,7 @@ require_permission, view_only) from redash.utils import json_dumps, render_custom_template + class AlertResource(BaseResource): def get(self, alert_id): alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org) @@ -133,10 +134,11 @@ def delete(self, alert_id, subscriber_id): 'object_type': 'alert' }) + class AlertTemplateResource(BaseResource): def post(self): req = request.get_json(True) template = req.get("template", "") data = req.get("data", "") preview, err = render_custom_template(template, data['rows'], data['columns'], True) - return json_dumps({'preview': preview, "error": err }) + return json_dumps({'preview': preview, "error": err}) diff --git a/redash/utils/__init__.py b/redash/utils/__init__.py index 2b2279bb4b..ba3b0835c8 100644 --- a/redash/utils/__init__.py +++ b/redash/utils/__init__.py @@ -113,6 +113,7 @@ 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) @@ -126,7 +127,7 @@ def render_custom_template(template, rows, columns, showError=None): return message, err else: message = e.message - return message, err + return message, err class UnicodeWriter: From 2133e43f104bdc6cf6f3aa26cffb625e3c2cda29 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Sat, 19 Jan 2019 22:49:01 +0900 Subject: [PATCH 06/19] add webhook alert description and avoid key error --- .../{ed7bf6adbd4d_.py => ed7bf6adbd4d_add_alert_template.py} | 2 +- redash/destinations/webhook.py | 3 ++- redash/handlers/alerts.py | 5 ++++- redash/models/__init__.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) rename migrations/versions/{ed7bf6adbd4d_.py => ed7bf6adbd4d_add_alert_template.py} (95%) diff --git a/migrations/versions/ed7bf6adbd4d_.py b/migrations/versions/ed7bf6adbd4d_add_alert_template.py similarity index 95% rename from migrations/versions/ed7bf6adbd4d_.py rename to migrations/versions/ed7bf6adbd4d_add_alert_template.py index 629bbee9ac..fe73b4ebde 100644 --- a/migrations/versions/ed7bf6adbd4d_.py +++ b/migrations/versions/ed7bf6adbd4d_add_alert_template.py @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = 'ed7bf6adbd4d' -down_revision = '71477dadd6ef' +down_revision = '73beceabb948' branch_labels = None depends_on = None diff --git a/redash/destinations/webhook.py b/redash/destinations/webhook.py index 5b572db080..e93aff35e1 100644 --- a/redash/destinations/webhook.py +++ b/redash/destinations/webhook.py @@ -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 diff --git a/redash/handlers/alerts.py b/redash/handlers/alerts.py index 1c5917b688..18e216a286 100644 --- a/redash/handlers/alerts.py +++ b/redash/handlers/alerts.py @@ -138,7 +138,10 @@ def delete(self, alert_id, subscriber_id): class AlertTemplateResource(BaseResource): def post(self): req = request.get_json(True) - template = req.get("template", "") 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}) diff --git a/redash/models/__init__.py b/redash/models/__init__.py index 90b70430ed..8c475b8880 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -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 From a9e72e79c4c27cd6864efadcd7c8f4c8ccc6bd11 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Tue, 5 Feb 2019 23:16:07 +0900 Subject: [PATCH 07/19] refactor: create alert template module --- client/app/pages/alert/index.js | 57 ++++++++------------------- client/app/services/alert-template.js | 48 ++++++++++++++++++++++ redash/handlers/api.py | 2 +- 3 files changed, 65 insertions(+), 42 deletions(-) create mode 100644 client/app/services/alert-template.js diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js index 96d9b21577..630d8bc728 100644 --- a/client/app/pages/alert/index.js +++ b/client/app/pages/alert/index.js @@ -1,49 +1,11 @@ import { template as templateBuilder } from 'lodash'; import template from './alert.html'; -function AlertCtrl($routeParams, $location, $sce, $http, toastr, currentUser, Query, Events, Alert) { +function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Events, Alert, AlertTemplate) { 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, '"') - .replace(/&/g, '&') - .replace(//g, '>'); - this.alert.previewHTML = $sce.trustAsHtml(replaced.replace(/\n|\r/g, '
')); - if (data.error) { - toastr.error('Unable to build description. please confirm your template.', { timeOut: 10000 }); - } - }) - .error(() => { - toastr.error('Failed. unexpected error.'); - }); - }; + this.templateHelpMsg = AlertTemplate.helpMessage; + this.editorOptions = AlertTemplate.editorOptions; if (this.alertId === 'new') { Events.record('view', 'page', 'alerts/new'); @@ -114,6 +76,19 @@ function AlertCtrl($routeParams, $location, $sce, $http, toastr, currentUser, Qu ); }; + this.preview = () => AlertTemplate.render(this.alert.template, this.queryResult.query_result.data) + .then((data) => { + if (data.error) { + toastr.error('Unable to build description. please confirm your template.', { timeOut: 10000 }); + return; + } + this.alert.preview = data.preview; + this.alert.previewHTML = $sce.trustAsHtml(data.preview); + }) + .catch(() => { + toastr.error('Failed. unexpected error.'); + }); + this.delete = () => { this.alert.$delete( () => { diff --git a/client/app/services/alert-template.js b/client/app/services/alert-template.js new file mode 100644 index 0000000000..4d75e82702 --- /dev/null +++ b/client/app/services/alert-template.js @@ -0,0 +1,48 @@ +import { $http } from '@/services/ng'; + +export let AlertTemplate = null; // eslint-disable-line import/no-mutable-exports + +function AlertTemplateService() { + const Alert = { + render: (template, queryResult) => { + const url = 'api/alerts/template'; + return $http + .post(url, { template, data: queryResult }) + .then((res) => { + const data = JSON.parse(res.data); + const preview = data.preview; + const error = data.error; + return { preview, error }; + }); + }, + helpMessage: `using template engine "Jinja2". + you can build message with latest query result. + variable name "rows" is assigned as result rows. "cols" as result columns.`, + editorOptions: { + useWrapMode: true, + showPrintMargin: false, + advanced: { + behavioursEnabled: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + autoScrollEditorIntoView: true, + }, + onLoad(editor) { + editor.$blockScrolling = Infinity; + }, + }, + }; + + return Alert; +} + + +export default function init(ngModule) { + ngModule.factory('AlertTemplate', AlertTemplateService); + + ngModule.run(($injector) => { + AlertTemplate = $injector.get('AlertTemplate'); + }); +} + +init.init = true; diff --git a/redash/handlers/api.py b/redash/handlers/api.py index b38a338156..f482925258 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -5,7 +5,7 @@ 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, AlertTemplateResource +from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource, AlertTemplateResource from redash.handlers.dashboards import DashboardListResource, DashboardResource, DashboardShareResource, PublicDashboardResource from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource from redash.handlers.events import EventsResource From 663c4b4a2f68a8369ce1d1dd6368976371d9b399 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Tue, 5 Feb 2019 23:22:17 +0900 Subject: [PATCH 08/19] follow code style --- redash/handlers/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redash/handlers/api.py b/redash/handlers/api.py index f482925258..92a4ec956a 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -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, AlertTemplateResource -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 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 From 7bea6ae6a656d0def420db1b1284729422fabdea Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Wed, 6 Feb 2019 00:45:39 +0900 Subject: [PATCH 09/19] use es6 class, fix template display --- client/app/pages/alert/alert.html | 8 ++-- client/app/pages/alert/index.js | 11 +++--- client/app/services/alert-template.js | 57 ++++++++++++--------------- 3 files changed, 34 insertions(+), 42 deletions(-) diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html index 972f0d72cf..4934b45375 100644 --- a/client/app/pages/alert/alert.html +++ b/client/app/pages/alert/alert.html @@ -57,9 +57,9 @@
- +
-
+
@@ -74,10 +74,10 @@
-
+
-
+
diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js index 630d8bc728..4709c8932d 100644 --- a/client/app/pages/alert/index.js +++ b/client/app/pages/alert/index.js @@ -1,11 +1,11 @@ import { template as templateBuilder } from 'lodash'; import template from './alert.html'; +import AlertTemplate from '../../services/alert-template'; -function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Events, Alert, AlertTemplate) { +function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Events, Alert) { this.alertId = $routeParams.alertId; this.hidePreview = false; - this.templateHelpMsg = AlertTemplate.helpMessage; - this.editorOptions = AlertTemplate.editorOptions; + this.alertTemplate = new AlertTemplate(); if (this.alertId === 'new') { Events.record('view', 'page', 'alerts/new'); @@ -76,13 +76,12 @@ function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Ev ); }; - this.preview = () => AlertTemplate.render(this.alert.template, this.queryResult.query_result.data) + this.preview = () => this.alertTemplate.render(this.alert.template, this.queryResult.query_result.data) .then((data) => { if (data.error) { toastr.error('Unable to build description. please confirm your template.', { timeOut: 10000 }); - return; } - this.alert.preview = data.preview; + this.alert.preview = $sce.trustAsHtml(data.previewEscaped); this.alert.previewHTML = $sce.trustAsHtml(data.preview); }) .catch(() => { diff --git a/client/app/services/alert-template.js b/client/app/services/alert-template.js index 4d75e82702..96ada883ec 100644 --- a/client/app/services/alert-template.js +++ b/client/app/services/alert-template.js @@ -1,24 +1,30 @@ import { $http } from '@/services/ng'; -export let AlertTemplate = null; // eslint-disable-line import/no-mutable-exports +export default class AlertTemplate { + render(template, queryResult) { + const url = 'api/alerts/template'; + return $http + .post(url, { template, data: queryResult }) + .then((res) => { + const data = JSON.parse(res.data); + const preview = data.preview; + const escaped = data.preview + .replace(/"/g, '"') + .replace(/&/g, '&') + .replace(//g, '>'); + const previewEscaped = escaped.replace(/\n|\r/g, '
'); + const error = data.error; + return { preview, previewEscaped, error }; + }); + } -function AlertTemplateService() { - const Alert = { - render: (template, queryResult) => { - const url = 'api/alerts/template'; - return $http - .post(url, { template, data: queryResult }) - .then((res) => { - const data = JSON.parse(res.data); - const preview = data.preview; - const error = data.error; - return { preview, error }; - }); - }, - helpMessage: `using template engine "Jinja2". + constructor() { + this.helpMessage = `using template engine "Jinja2". you can build message with latest query result. - variable name "rows" is assigned as result rows. "cols" as result columns.`, - editorOptions: { + variable name "rows" is assigned as result rows. "cols" as result columns.`; + + this.editorOptions = { useWrapMode: true, showPrintMargin: false, advanced: { @@ -30,19 +36,6 @@ function AlertTemplateService() { onLoad(editor) { editor.$blockScrolling = Infinity; }, - }, - }; - - return Alert; + }; + } } - - -export default function init(ngModule) { - ngModule.factory('AlertTemplate', AlertTemplateService); - - ngModule.run(($injector) => { - AlertTemplate = $injector.get('AlertTemplate'); - }); -} - -init.init = true; From 570ce3fc33c76d3aa63da58d101e3dfb58ecc2e2 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Tue, 26 Mar 2019 03:46:27 +0900 Subject: [PATCH 10/19] use alerts.options, use mustache --- client/app/pages/alert/alert.html | 2 +- client/app/pages/alert/index.js | 24 +++++++++++-------- client/app/services/alert-template.js | 33 +++++++++++++-------------- redash/destinations/chatwork.py | 5 +++- redash/destinations/slack.py | 4 ++-- redash/handlers/alerts.py | 16 ++----------- redash/handlers/api.py | 3 +-- redash/models/__init__.py | 8 +++---- redash/serializers.py | 3 +-- redash/utils/__init__.py | 15 ------------ tests/handlers/test_alerts.py | 3 +-- 11 files changed, 46 insertions(+), 70 deletions(-) diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html index b2442effb5..43798a1f89 100644 --- a/client/app/pages/alert/alert.html +++ b/client/app/pages/alert/alert.html @@ -61,7 +61,7 @@
-
+
diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js index 4709c8932d..7849e32131 100644 --- a/client/app/pages/alert/index.js +++ b/client/app/pages/alert/index.js @@ -76,17 +76,21 @@ function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Ev ); }; - this.preview = () => this.alertTemplate.render(this.alert.template, this.queryResult.query_result.data) - .then((data) => { - if (data.error) { - toastr.error('Unable to build description. please confirm your template.', { timeOut: 10000 }); + this.preview = () => { + const notifyError = () => toastr.error('Unable to render description. please confirm your template.', { timeOut: 10000 }); + try { + const result = this.alertTemplate.render(this.alert.options.template, this.queryResult.query_result.data); + this.alert.preview = $sce.trustAsHtml(result.escaped); + this.alert.previewHTML = $sce.trustAsHtml(result.raw); + if (!result.raw) { + notifyError(); } - this.alert.preview = $sce.trustAsHtml(data.previewEscaped); - this.alert.previewHTML = $sce.trustAsHtml(data.preview); - }) - .catch(() => { - toastr.error('Failed. unexpected error.'); - }); + } catch (e) { + notifyError(); + this.alert.preview = e.message; + this.alert.previewHTML = e.message; + } + }; this.delete = () => { this.alert.$delete( diff --git a/client/app/services/alert-template.js b/client/app/services/alert-template.js index 96ada883ec..6120f2b84b 100644 --- a/client/app/services/alert-template.js +++ b/client/app/services/alert-template.js @@ -1,26 +1,25 @@ -import { $http } from '@/services/ng'; +// import { $http } from '@/services/ng'; +import Mustache from 'mustache'; export default class AlertTemplate { render(template, queryResult) { - const url = 'api/alerts/template'; - return $http - .post(url, { template, data: queryResult }) - .then((res) => { - const data = JSON.parse(res.data); - const preview = data.preview; - const escaped = data.preview - .replace(/"/g, '"') - .replace(/&/g, '&') - .replace(//g, '>'); - const previewEscaped = escaped.replace(/\n|\r/g, '
'); - const error = data.error; - return { preview, previewEscaped, error }; - }); + const view = { + rows: queryResult.rows, + cols: queryResult.columns, + }; + const result = Mustache.render(template, view); + const escaped = result + .replace(/"/g, '"') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n|\r/g, '
'); + + return { escaped, raw: result }; } constructor() { - this.helpMessage = `using template engine "Jinja2". + this.helpMessage = `using template engine "mustache". you can build message with latest query result. variable name "rows" is assigned as result rows. "cols" as result columns.`; diff --git a/redash/destinations/chatwork.py b/redash/destinations/chatwork.py index 6d794b13ed..0826261ae0 100644 --- a/redash/destinations/chatwork.py +++ b/redash/destinations/chatwork.py @@ -47,7 +47,10 @@ def notify(self, alert, query, user, new_state, app, host, options): 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} diff --git a/redash/destinations/slack.py b/redash/destinations/slack.py index 1740a76465..8eea18fd2d 100644 --- a/redash/destinations/slack.py +++ b/redash/destinations/slack.py @@ -55,8 +55,8 @@ 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) + if 'template' in alert.options: + description = alert.render_template() fields.append({ "title": "Description", "value": description diff --git a/redash/handlers/alerts.py b/redash/handlers/alerts.py index 18e216a286..61e7c31e96 100644 --- a/redash/handlers/alerts.py +++ b/redash/handlers/alerts.py @@ -9,7 +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 +from redash.utils import json_dumps class AlertResource(BaseResource): @@ -25,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', 'template')) + params = project(req, ('options', 'name', 'query_id', 'rearm')) alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org) require_admin_or_owner(alert.user.id) @@ -62,7 +62,6 @@ def post(self): user=self.current_user, rearm=req.get('rearm'), options=req['options'], - template=req['template'] ) models.db.session.add(alert) @@ -134,14 +133,3 @@ def delete(self, alert_id, subscriber_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}) diff --git a/redash/handlers/api.py b/redash/handlers/api.py index 20af946a44..1fef2f5409 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -5,7 +5,7 @@ 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, AlertTemplateResource +from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource from redash.handlers.dashboards import DashboardListResource, DashboardResource, DashboardShareResource, PublicDashboardResource from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource from redash.handlers.events import EventsResource @@ -44,7 +44,6 @@ def json_representation(data, code, headers=None): api.add_org_resource(AlertResource, '/api/alerts/', endpoint='alert') -api.add_org_resource(AlertTemplateResource, '/api/alerts/template', endpoint='alert_template') api.add_org_resource(AlertSubscriptionListResource, '/api/alerts//subscriptions', endpoint='alert_subscriptions') api.add_org_resource(AlertSubscriptionResource, '/api/alerts//subscriptions/', endpoint='alert_subscription') api.add_org_resource(AlertListResource, '/api/alerts', endpoint='alerts') diff --git a/redash/models/__init__.py b/redash/models/__init__.py index 4642e74ece..adbcd39c2a 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -25,7 +25,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, render_custom_template +from redash.utils import generate_token, json_dumps, json_loads, mustache_render from redash.utils.configuration import ConfigurationContainer from .base import db, gfk_type, Column, GFKBase, SearchBaseQuery @@ -726,7 +726,6 @@ 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' @@ -773,9 +772,10 @@ def evaluate(self): def subscribers(self): return User.query.join(AlertSubscription).filter(AlertSubscription.alert == self) - def render_template(self, showError=None): + def render_template(self): data = json_loads(self.query_rel.latest_query_data.data) - return render_custom_template(self.template, data['rows'], data['columns']) + context = {'rows': data['rows'], 'cols': data['columns']} + return mustache_render(self.options['template'], context) @property def groups(self): diff --git a/redash/serializers.py b/redash/serializers.py index 9a059699cd..d809a1f73e 100644 --- a/redash/serializers.py +++ b/redash/serializers.py @@ -170,8 +170,7 @@ 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, - 'template': alert.template + 'rearm': alert.rearm } if full: diff --git a/redash/utils/__init__.py b/redash/utils/__init__.py index 2e0ed037ea..c87150dddb 100644 --- a/redash/utils/__init__.py +++ b/redash/utils/__init__.py @@ -18,7 +18,6 @@ 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 @@ -132,20 +131,6 @@ 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: diff --git a/tests/handlers/test_alerts.py b/tests/handlers/test_alerts.py index a36435601b..638595cb0d 100644 --- a/tests/handlers/test_alerts.py +++ b/tests/handlers/test_alerts.py @@ -94,10 +94,9 @@ 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, template="alert-template")) + rearm=100)) 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()) From 4efb59fffbb3e3831aeab4caacac4086df3dd156 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Tue, 26 Mar 2019 05:48:52 +0900 Subject: [PATCH 11/19] fix email description --- redash/destinations/email.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redash/destinations/email.py b/redash/destinations/email.py index fa804542e1..c2fe3475db 100644 --- a/redash/destinations/email.py +++ b/redash/destinations/email.py @@ -37,8 +37,8 @@ def notify(self, alert, query, user, new_state, app, host, options): html = """ Check alert / check query
. """.format(host=host, alert_id=alert.id, query_id=query.id) - if alert.template: - description, _ = alert.render_template() + if 'template' in alert.options: + description = alert.render_template() html += "
" + description logging.debug("Notifying: %s", recipients) From c96f84f6e83f71d9bcc2de3570b54042e7497ed8 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Tue, 26 Mar 2019 06:47:21 +0900 Subject: [PATCH 12/19] alert custom subject --- client/app/pages/alert/alert.html | 6 +++- .../ed7bf6adbd4d_add_alert_template.py | 28 ------------------- redash/destinations/chatwork.py | 4 +-- redash/destinations/email.py | 11 ++++++-- redash/destinations/slack.py | 7 +++-- redash/destinations/webhook.py | 2 +- redash/handlers/alerts.py | 1 - redash/models/__init__.py | 14 ++++++++++ redash/utils/__init__.py | 2 -- 9 files changed, 35 insertions(+), 40 deletions(-) delete mode 100644 migrations/versions/ed7bf6adbd4d_add_alert_template.py diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html index 43798a1f89..e39572e0b9 100644 --- a/client/app/pages/alert/alert.html +++ b/client/app/pages/alert/alert.html @@ -56,9 +56,13 @@
+
+ + +
- +
diff --git a/migrations/versions/ed7bf6adbd4d_add_alert_template.py b/migrations/versions/ed7bf6adbd4d_add_alert_template.py deleted file mode 100644 index fe73b4ebde..0000000000 --- a/migrations/versions/ed7bf6adbd4d_add_alert_template.py +++ /dev/null @@ -1,28 +0,0 @@ -"""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)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('alerts', 'template') - # ### end Alembic commands ### diff --git a/redash/destinations/chatwork.py b/redash/destinations/chatwork.py index 0826261ae0..ed1f214cea 100644 --- a/redash/destinations/chatwork.py +++ b/redash/destinations/chatwork.py @@ -47,9 +47,9 @@ def notify(self, alert, query, user, new_state, app, host, options): alert_name=alert.name, new_state=new_state.upper(), alert_url=alert_url, query_url=query_url) - + if alert.template: - description, _ = alert.render_template() + description = alert.render_template() message = message + "\n" + description headers = {'X-ChatWorkToken': options.get('api_token')} payload = {'body': message} diff --git a/redash/destinations/email.py b/redash/destinations/email.py index c2fe3475db..537ec6bc2b 100644 --- a/redash/destinations/email.py +++ b/redash/destinations/email.py @@ -37,7 +37,7 @@ def notify(self, alert, query, user, new_state, app, host, options): html = """ Check alert / check query
. """.format(host=host, alert_id=alert.id, query_id=query.id) - if 'template' in alert.options: + if alert.template: description = alert.render_template() html += "
" + description logging.debug("Notifying: %s", recipients) @@ -45,10 +45,15 @@ def notify(self, alert, query, user, new_state, app, host, options): try: alert_name = alert.name.encode('utf-8', 'ignore') state = new_state.upper() - subject_template = options.get('subject_template', settings.ALERTS_DEFAULT_MAIL_SUBJECT_TEMPLATE) + if alert.custom_subject: + subject = alert.custom_subject + else: + subject_template = options.get('subject_template', settings.ALERTS_DEFAULT_MAIL_SUBJECT_TEMPLATE) + subject = subject_template.format(alert_name=alert_name, state=state) + message = Message( recipients=recipients, - subject=subject_template.format(alert_name=alert_name, state=state), + subject=subject, html=html ) mail.send(message) diff --git a/redash/destinations/slack.py b/redash/destinations/slack.py index 8eea18fd2d..d3e5855150 100644 --- a/redash/destinations/slack.py +++ b/redash/destinations/slack.py @@ -53,9 +53,12 @@ def notify(self, alert, query, user, new_state, app, host, options): } ] if new_state == "triggered": - text = alert.name + " just triggered" + if alert.custom_subject: + text = alert.custom_subject + else: + text = alert.name + " just triggered" color = "#c0392b" - if 'template' in alert.options: + if alert.template: description = alert.render_template() fields.append({ "title": "Description", diff --git a/redash/destinations/webhook.py b/redash/destinations/webhook.py index e93aff35e1..07d26e2896 100644 --- a/redash/destinations/webhook.py +++ b/redash/destinations/webhook.py @@ -37,7 +37,7 @@ def notify(self, alert, query, user, new_state, app, host, options): 'event': 'alert_state_change', 'alert': serialize_alert(alert, full=False), 'url_base': host, - "description": alert.render_template(True) if alert.template else '' + "description": alert.render_template() if alert.template else '' } headers = {'Content-Type': 'application/json'} auth = HTTPBasicAuth(options.get('username'), options.get('password')) if options.get('username') else None diff --git a/redash/handlers/alerts.py b/redash/handlers/alerts.py index 61e7c31e96..866aa9448b 100644 --- a/redash/handlers/alerts.py +++ b/redash/handlers/alerts.py @@ -132,4 +132,3 @@ def delete(self, alert_id, subscriber_id): 'object_id': alert_id, 'object_type': 'alert' }) - diff --git a/redash/models/__init__.py b/redash/models/__init__.py index adbcd39c2a..9a703353c4 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -777,6 +777,20 @@ def render_template(self): context = {'rows': data['rows'], 'cols': data['columns']} return mustache_render(self.options['template'], context) + @property + def template(self): + if 'template' in self.options: + return self.options['template'] + else: + return "" + + @property + def custom_subject(self): + if 'subject' in self.options: + return self.options['subject'] + else: + return "" + @property def groups(self): return self.query_rel.groups diff --git a/redash/utils/__init__.py b/redash/utils/__init__.py index c87150dddb..eb99caba5d 100644 --- a/redash/utils/__init__.py +++ b/redash/utils/__init__.py @@ -131,8 +131,6 @@ def build_url(request, host, path): return "{}://{}{}".format(request.scheme, host, path) - - class UnicodeWriter: """ A CSV writer which will write rows to CSV file "f", From 04c2474f54593f999a584195e0cdbbe631217bb6 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Thu, 28 Mar 2019 09:59:04 +0900 Subject: [PATCH 13/19] add alert state to template context, sanitized preview --- client/app/pages/alert/index.js | 8 ++++---- client/app/services/alert-template.js | 8 +++++--- redash/destinations/slack.py | 12 ++++++------ redash/destinations/webhook.py | 2 +- redash/models/__init__.py | 16 ++++++---------- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js index 5305a9a781..86918792df 100644 --- a/client/app/pages/alert/index.js +++ b/client/app/pages/alert/index.js @@ -3,7 +3,7 @@ import notification from '@/services/notification'; import template from './alert.html'; import AlertTemplate from '../../services/alert-template'; -function AlertCtrl($scope, $routeParams, $location, $sce, currentUser, Query, Events, Alert) { +function AlertCtrl($scope, $routeParams, $location, $sce, $sanitize, currentUser, Query, Events, Alert) { this.alertId = $routeParams.alertId; this.hidePreview = false; this.alertTemplate = new AlertTemplate(); @@ -80,11 +80,11 @@ function AlertCtrl($scope, $routeParams, $location, $sce, currentUser, Query, Ev }; this.preview = () => { - const notifyError = () => toastr.error('Unable to render description. please confirm your template.', { timeOut: 10000 }); + const notifyError = () => notification.error('Unable to render description. please confirm your template.'); try { - const result = this.alertTemplate.render(this.alert.options.template, this.queryResult.query_result.data); + const result = this.alertTemplate.render(this.alert, this.queryResult.query_result.data); this.alert.preview = $sce.trustAsHtml(result.escaped); - this.alert.previewHTML = $sce.trustAsHtml(result.raw); + this.alert.previewHTML = $sce.trustAsHtml($sanitize(result.raw)); if (!result.raw) { notifyError(); } diff --git a/client/app/services/alert-template.js b/client/app/services/alert-template.js index 6120f2b84b..dc2f3697da 100644 --- a/client/app/services/alert-template.js +++ b/client/app/services/alert-template.js @@ -2,12 +2,14 @@ import Mustache from 'mustache'; export default class AlertTemplate { - render(template, queryResult) { + render(alert, queryResult) { + console.log(alert); const view = { + state: alert.state, rows: queryResult.rows, cols: queryResult.columns, }; - const result = Mustache.render(template, view); + const result = Mustache.render(alert.options.template, view); const escaped = result .replace(/"/g, '"') .replace(/&/g, '&') @@ -21,7 +23,7 @@ export default class AlertTemplate { constructor() { this.helpMessage = `using template engine "mustache". you can build message with latest query result. - variable name "rows" is assigned as result rows. "cols" as result columns.`; + variable name "rows" is assigned as result rows. "cols" as result columns, "state" as alert state.`; this.editorOptions = { useWrapMode: true, diff --git a/redash/destinations/slack.py b/redash/destinations/slack.py index d3e5855150..9cbcfda2c5 100644 --- a/redash/destinations/slack.py +++ b/redash/destinations/slack.py @@ -52,18 +52,18 @@ def notify(self, alert, query, user, new_state, app, host, options): "short": True } ] + if alert.template: + description = alert.render_template() + fields.append({ + "title": "Description", + "value": description + }) if new_state == "triggered": if alert.custom_subject: text = alert.custom_subject else: text = alert.name + " just triggered" color = "#c0392b" - if alert.template: - description = alert.render_template() - fields.append({ - "title": "Description", - "value": description - }) else: text = alert.name + " went back to normal" color = "#27ae60" diff --git a/redash/destinations/webhook.py b/redash/destinations/webhook.py index 07d26e2896..e3cfe37689 100644 --- a/redash/destinations/webhook.py +++ b/redash/destinations/webhook.py @@ -37,7 +37,7 @@ def notify(self, alert, query, user, new_state, app, host, options): 'event': 'alert_state_change', 'alert': serialize_alert(alert, full=False), 'url_base': host, - "description": alert.render_template() if alert.template else '' + "description": alert.render_template() } headers = {'Content-Type': 'application/json'} auth = HTTPBasicAuth(options.get('username'), options.get('password')) if options.get('username') else None diff --git a/redash/models/__init__.py b/redash/models/__init__.py index 87451297ee..b2bcdec640 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -793,23 +793,19 @@ def subscribers(self): return User.query.join(AlertSubscription).filter(AlertSubscription.alert == self) def render_template(self): + if not self.template: + return '' data = json_loads(self.query_rel.latest_query_data.data) - context = {'rows': data['rows'], 'cols': data['columns']} - return mustache_render(self.options['template'], context) + context = {'rows': data['rows'], 'cols': data['columns'], 'state': self.state} + return mustache_render(self.template, context) @property def template(self): - if 'template' in self.options: - return self.options['template'] - else: - return "" + return self.options.get('template', '') @property def custom_subject(self): - if 'subject' in self.options: - return self.options['subject'] - else: - return "" + return self.options.get('subject', '') @property def groups(self): From d8ef11dc1b8fee694911830dba588525cd185109 Mon Sep 17 00:00:00 2001 From: kota_tomoyasu Date: Thu, 28 Mar 2019 10:54:59 +0900 Subject: [PATCH 14/19] remove console.log :bow: --- client/app/services/alert-template.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/app/services/alert-template.js b/client/app/services/alert-template.js index dc2f3697da..74da808326 100644 --- a/client/app/services/alert-template.js +++ b/client/app/services/alert-template.js @@ -3,7 +3,6 @@ import Mustache from 'mustache'; export default class AlertTemplate { render(alert, queryResult) { - console.log(alert); const view = { state: alert.state, rows: queryResult.rows, From d6102d203c271dda00aba6a77afd76f8f3201d93 Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Fri, 29 Mar 2019 09:32:08 +0900 Subject: [PATCH 15/19] chatwork custom_subject --- redash/destinations/chatwork.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/redash/destinations/chatwork.py b/redash/destinations/chatwork.py index ed1f214cea..c4751f25d8 100644 --- a/redash/destinations/chatwork.py +++ b/redash/destinations/chatwork.py @@ -40,10 +40,11 @@ def notify(self, alert, query, user, new_state, app, host, options): 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( + message = '' + if alert.custom_subject: + message = alert.custom_subject + '\n' + message += message_template.replace('\\n', '\n').format( alert_name=alert.name, new_state=new_state.upper(), alert_url=alert_url, query_url=query_url) From ba644039437ba1f38a046cf832e9c7e9554d67ab Mon Sep 17 00:00:00 2001 From: Kota Tomoyasu Date: Sat, 13 Apr 2019 11:24:04 +0900 Subject: [PATCH 16/19] add alert custom message. pagerduty, mattermost, hangoutschat --- redash/destinations/hangoutschat.py | 18 +++++++++++++++++- redash/destinations/mattermost.py | 9 +++++++++ redash/destinations/pagerduty.py | 7 ++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/redash/destinations/hangoutschat.py b/redash/destinations/hangoutschat.py index 28951b6193..5db48e9cf7 100644 --- a/redash/destinations/hangoutschat.py +++ b/redash/destinations/hangoutschat.py @@ -44,11 +44,16 @@ def notify(self, alert, query, user, new_state, app, host, options): else: message = "Unable to determine status. Check Query and Alert configuration." + if alert.custom_subject: + title = alert.custom_subject + else: + title = alert.name + data = { "cards": [ { "header": { - "title": alert.name + "title": title }, "sections": [ { @@ -65,6 +70,17 @@ def notify(self, alert, query, user, new_state, app, host, options): ] } + if alert.template: + data["cards"][0]["sections"].append({ + "widgets": [ + { + "textParagraph": { + "text": alert.render_template() + } + } + ] + }) + if options.get("icon_url"): data["cards"][0]["header"]["imageUrl"] = options.get("icon_url") diff --git a/redash/destinations/mattermost.py b/redash/destinations/mattermost.py index ea0280954d..ca9e4ea161 100644 --- a/redash/destinations/mattermost.py +++ b/redash/destinations/mattermost.py @@ -40,7 +40,16 @@ def notify(self, alert, query, user, new_state, app, host, options): else: text = "####" + alert.name + " went back to normal" + if alert.custom_subject: + text += '\n' + alert.custom_subject payload = {'text': text} + + if alert.template: + payload['attachments'] = [{'fields': [{ + "title": "Description", + "value": alert.render_template() + }]}] + if options.get('username'): payload['username'] = options.get('username') if options.get('icon_url'): payload['icon_url'] = options.get('icon_url') if options.get('channel'): payload['channel'] = options.get('channel') diff --git a/redash/destinations/pagerduty.py b/redash/destinations/pagerduty.py index c0411a04ff..737d801e57 100644 --- a/redash/destinations/pagerduty.py +++ b/redash/destinations/pagerduty.py @@ -43,7 +43,9 @@ def notify(self, alert, query, user, new_state, app, host, options): default_desc = self.DESCRIPTION_STR.format(query_id=query.id, query_name=query.name) - if options.get('description'): + if alert.custom_subject: + default_desc = alert.custom_subject + elif options.get('description'): default_desc = options.get('description') incident_key = self.KEY_STRING.format(alert_id=alert.id, query_id=query.id) @@ -58,6 +60,9 @@ def notify(self, alert, query, user, new_state, app, host, options): } } + if alert.template: + data['payload']['custom_details'] = alert.render_template() + if new_state == 'triggered': data['event_action'] = 'trigger' elif new_state == "unknown": From b078866bfd48c96b342bbb07e4a26b70ae8b6986 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Thu, 11 Jul 2019 12:22:29 +0300 Subject: [PATCH 17/19] Pass custom subject in webhook destination --- redash/destinations/webhook.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/redash/destinations/webhook.py b/redash/destinations/webhook.py index e3cfe37689..eb0cd06e0b 100644 --- a/redash/destinations/webhook.py +++ b/redash/destinations/webhook.py @@ -37,8 +37,11 @@ def notify(self, alert, query, user, new_state, app, host, options): 'event': 'alert_state_change', 'alert': serialize_alert(alert, full=False), 'url_base': host, - "description": alert.render_template() } + + data['alert']['description'] = alert.render_template() + data['alert']['title'] = alert.custom_subject + headers = {'Content-Type': 'application/json'} auth = HTTPBasicAuth(options.get('username'), options.get('password')) if options.get('username') else None resp = requests.post(options.get('url'), data=json_dumps(data), auth=auth, headers=headers, timeout=5.0) From 60cb8581747d03ee1077e0ce7abb3d9e5e60eeae Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Thu, 11 Jul 2019 12:22:58 +0300 Subject: [PATCH 18/19] Add log message when checking alert. --- redash/tasks/alerts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/redash/tasks/alerts.py b/redash/tasks/alerts.py index 7a239cf6e4..2346b9dee1 100644 --- a/redash/tasks/alerts.py +++ b/redash/tasks/alerts.py @@ -40,6 +40,7 @@ def check_alerts_for_query(query_id): query = models.Query.query.get(query_id) for alert in query.alerts: + logger.info("Checking alert (%d) of query %d.", alert.id, query_id) new_state = alert.evaluate() if should_notify(alert, new_state): From a96618f3e4d64178a9f5636c31f4c123bcb9c266 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Thu, 11 Jul 2019 12:23:43 +0300 Subject: [PATCH 19/19] Add feature flag for extra alert options. --- client/app/pages/alert/alert.html | 4 ++-- client/app/pages/alert/index.js | 2 ++ redash/handlers/authentication.py | 1 + redash/settings/__init__.py | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html index 0afd7ceef3..cf8c86f474 100644 --- a/client/app/pages/alert/alert.html +++ b/client/app/pages/alert/alert.html @@ -47,11 +47,11 @@
-
+
-
+
diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js index 24e8fda88c..1296468222 100644 --- a/client/app/pages/alert/index.js +++ b/client/app/pages/alert/index.js @@ -3,12 +3,14 @@ import notification from '@/services/notification'; import Modal from 'antd/lib/modal'; import template from './alert.html'; import AlertTemplate from '@/services/alert-template'; +import { clientConfig } from '@/services/auth'; import navigateTo from '@/services/navigateTo'; function AlertCtrl($scope, $routeParams, $location, $sce, $sanitize, currentUser, Query, Events, Alert) { this.alertId = $routeParams.alertId; this.hidePreview = false; this.alertTemplate = new AlertTemplate(); + this.showExtendedOptions = clientConfig.extendedAlertOptions; if (this.alertId === 'new') { Events.record('view', 'page', 'alerts/new'); diff --git a/redash/handlers/authentication.py b/redash/handlers/authentication.py index 2ace2d3e26..461e033e5a 100644 --- a/redash/handlers/authentication.py +++ b/redash/handlers/authentication.py @@ -227,6 +227,7 @@ def client_config(): 'showPermissionsControl': current_org.get_setting("feature_show_permissions_control"), 'allowCustomJSVisualizations': settings.FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS, 'autoPublishNamedQueries': settings.FEATURE_AUTO_PUBLISH_NAMED_QUERIES, + 'extendedAlertOptions': settings.FEATURE_EXTENDED_ALERT_OPTIONS, 'mailSettingsMissing': not settings.email_server_is_configured(), 'dashboardRefreshIntervals': settings.DASHBOARD_REFRESH_INTERVALS, 'queryRefreshIntervals': settings.QUERY_REFRESH_INTERVALS, diff --git a/redash/settings/__init__.py b/redash/settings/__init__.py index c7b57ff85c..3955a26d01 100644 --- a/redash/settings/__init__.py +++ b/redash/settings/__init__.py @@ -333,6 +333,7 @@ def email_server_is_configured(): FEATURE_SHOW_QUERY_RESULTS_COUNT = parse_boolean(os.environ.get("REDASH_FEATURE_SHOW_QUERY_RESULTS_COUNT", "true")) FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS = parse_boolean(os.environ.get("REDASH_FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS", "false")) FEATURE_AUTO_PUBLISH_NAMED_QUERIES = parse_boolean(os.environ.get("REDASH_FEATURE_AUTO_PUBLISH_NAMED_QUERIES", "true")) +FEATURE_EXTENDED_ALERT_OPTIONS = parse_boolean(os.environ.get("REDASH_FEATURE_EXTENDED_ALERT_OPTIONS", "false")) # BigQuery BIGQUERY_HTTP_TIMEOUT = int(os.environ.get("REDASH_BIGQUERY_HTTP_TIMEOUT", "600"))