From 21ddc8724bbd693112e66b26e45f2372bf9dd78a Mon Sep 17 00:00:00 2001 From: Darian Morrison Date: Wed, 1 Jul 2026 12:37:08 -0700 Subject: [PATCH 1/2] feat(alerts): add opt-in "Allow HTML content" for custom templates Values interpolated into a custom alert subject/body (query results and other {{...}} variables) are HTML-escaped by default. That is correct for the HTML email body but turns links and markup from query results into literal text in other destinations. Add an "Allow HTML content" checkbox to the alert template editor, mirroring the table visualization's per-column toggle, that renders the custom template without escaping. Escaping stays on by default, and the flag only affects the author's custom template -- the system default mail body is always escaped. --- client/app/pages/alert/AlertEdit.jsx | 2 ++ client/app/pages/alert/AlertNew.jsx | 2 ++ .../alert/components/NotificationTemplate.jsx | 29 ++++++++++++++++++- .../components/NotificationTemplate.less | 11 +++++++ redash/models/__init__.py | 9 +++--- tests/models/test_alerts.py | 23 +++++++++++++++ 6 files changed, 71 insertions(+), 5 deletions(-) diff --git a/client/app/pages/alert/AlertEdit.jsx b/client/app/pages/alert/AlertEdit.jsx index 64bff87631..be3446c3ac 100644 --- a/client/app/pages/alert/AlertEdit.jsx +++ b/client/app/pages/alert/AlertEdit.jsx @@ -103,6 +103,8 @@ export default class AlertEdit extends React.Component { setSubject={subject => onNotificationTemplateChange({ custom_subject: subject })} body={options.custom_body} setBody={body => onNotificationTemplateChange({ custom_body: body })} + allowHtml={options.allow_html} + setAllowHtml={allowHtml => onNotificationTemplateChange({ allow_html: allowHtml })} /> diff --git a/client/app/pages/alert/AlertNew.jsx b/client/app/pages/alert/AlertNew.jsx index 9e128ed2fc..1392b8ceeb 100644 --- a/client/app/pages/alert/AlertNew.jsx +++ b/client/app/pages/alert/AlertNew.jsx @@ -70,6 +70,8 @@ export default class AlertNew extends React.Component { setSubject={subject => onNotificationTemplateChange({ custom_subject: subject })} body={options.custom_body} setBody={body => onNotificationTemplateChange({ custom_body: body })} + allowHtml={options.allow_html} + setAllowHtml={allowHtml => onNotificationTemplateChange({ allow_html: allowHtml })} /> diff --git a/client/app/pages/alert/components/NotificationTemplate.jsx b/client/app/pages/alert/components/NotificationTemplate.jsx index 5aa750207b..173c88f887 100644 --- a/client/app/pages/alert/components/NotificationTemplate.jsx +++ b/client/app/pages/alert/components/NotificationTemplate.jsx @@ -4,12 +4,14 @@ import { head, isEmpty, isNull, isUndefined } from "lodash"; import Mustache from "mustache"; import HelpTrigger from "@/components/HelpTrigger"; +import Tooltip from "@/components/Tooltip"; 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 Checkbox from "antd/lib/checkbox"; import "./NotificationTemplate.less"; @@ -30,7 +32,18 @@ function normalizeCustomTemplateData(alert, query, columnNames, resultValues) { }; } -function NotificationTemplate({ alert, query, columnNames, resultValues, subject, setSubject, body, setBody }) { +function NotificationTemplate({ + alert, + query, + columnNames, + resultValues, + subject, + setSubject, + body, + setBody, + allowHtml, + setAllowHtml, +}) { const hasContent = !!(subject || body); const [enabled, setEnabled] = useState(hasContent ? 1 : 0); const [showPreview, setShowPreview] = useState(false); @@ -49,6 +62,7 @@ function NotificationTemplate({ alert, query, columnNames, resultValues, subject onOk: () => { setSubject(null); setBody(null); + setAllowHtml(false); setEnabled(value); setShowPreview(false); }, @@ -100,6 +114,16 @@ function NotificationTemplate({ alert, query, columnNames, resultValues, subject