Skip to content

Commit 660d04b

Browse files
authored
Adding Evaluate button for alerts to test them (#7032)
1 parent fc1e1f7 commit 660d04b

File tree

6 files changed

+73
-3
lines changed

6 files changed

+73
-3
lines changed

client/app/pages/alert/Alert.jsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import MenuButton from "./components/MenuButton";
1616
import AlertView from "./AlertView";
1717
import AlertEdit from "./AlertEdit";
1818
import AlertNew from "./AlertNew";
19+
import notifications from "@/services/notifications";
1920

2021
const MODES = {
2122
NEW: 0,
@@ -178,6 +179,17 @@ class Alert extends React.Component {
178179
});
179180
};
180181

182+
evaluate = () => {
183+
const { alert } = this.state;
184+
return AlertService.evaluate(alert)
185+
.then(() => {
186+
notification.success("Alert evaluated. Refresh page for updated status.");
187+
})
188+
.catch(() => {
189+
notifications.error("Failed to evaluate alert.");
190+
});
191+
};
192+
181193
mute = () => {
182194
const { alert } = this.state;
183195
return AlertService.mute(alert)
@@ -224,7 +236,14 @@ class Alert extends React.Component {
224236
const { queryResult, mode, canEdit, pendingRearm } = this.state;
225237

226238
const menuButton = (
227-
<MenuButton doDelete={this.delete} muted={muted} mute={this.mute} unmute={this.unmute} canEdit={canEdit} />
239+
<MenuButton
240+
doDelete={this.delete}
241+
muted={muted}
242+
mute={this.mute}
243+
unmute={this.unmute}
244+
canEdit={canEdit}
245+
evaluate={this.evaluate}
246+
/>
228247
);
229248

230249
const commonProps = {

client/app/pages/alert/components/MenuButton.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import LoadingOutlinedIcon from "@ant-design/icons/LoadingOutlined";
1111
import EllipsisOutlinedIcon from "@ant-design/icons/EllipsisOutlined";
1212
import PlainButton from "@/components/PlainButton";
1313

14-
export default function MenuButton({ doDelete, canEdit, mute, unmute, muted }) {
14+
export default function MenuButton({ doDelete, canEdit, mute, unmute, evaluate, muted }) {
1515
const [loading, setLoading] = useState(false);
1616

1717
const execute = useCallback(action => {
@@ -55,6 +55,9 @@ export default function MenuButton({ doDelete, canEdit, mute, unmute, muted }) {
5555
<Menu.Item>
5656
<PlainButton onClick={confirmDelete}>Delete</PlainButton>
5757
</Menu.Item>
58+
<Menu.Item>
59+
<PlainButton onClick={() => execute(evaluate)}>Evaluate</PlainButton>
60+
</Menu.Item>
5861
</Menu>
5962
}>
6063
<Button aria-label="More actions">
@@ -69,6 +72,7 @@ MenuButton.propTypes = {
6972
canEdit: PropTypes.bool.isRequired,
7073
mute: PropTypes.func.isRequired,
7174
unmute: PropTypes.func.isRequired,
75+
evaluate: PropTypes.func.isRequired,
7276
muted: PropTypes.bool,
7377
};
7478

client/app/services/alert.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const Alert = {
3636
delete: data => axios.delete(`api/alerts/${data.id}`),
3737
mute: data => axios.post(`api/alerts/${data.id}/mute`),
3838
unmute: data => axios.delete(`api/alerts/${data.id}/mute`),
39+
evaluate: data => axios.post(`api/alerts/${data.id}/eval`),
3940
};
4041

4142
export default Alert;

redash/handlers/alerts.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from flask import request
22
from funcy import project
33

4-
from redash import models
4+
from redash import models, utils
55
from redash.handlers.base import (
66
BaseResource,
77
get_object_or_404,
@@ -14,6 +14,10 @@
1414
view_only,
1515
)
1616
from redash.serializers import serialize_alert
17+
from redash.tasks.alerts import (
18+
notify_subscriptions,
19+
should_notify,
20+
)
1721

1822

1923
class AlertResource(BaseResource):
@@ -43,6 +47,21 @@ def delete(self, alert_id):
4347
models.db.session.commit()
4448

4549

50+
class AlertEvaluateResource(BaseResource):
51+
def post(self, alert_id):
52+
alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org)
53+
require_admin_or_owner(alert.user.id)
54+
55+
new_state = alert.evaluate()
56+
if should_notify(alert, new_state):
57+
alert.state = new_state
58+
alert.last_triggered_at = utils.utcnow()
59+
models.db.session.commit()
60+
61+
notify_subscriptions(alert, new_state, {})
62+
self.record_event({"action": "evaluate", "object_id": alert.id, "object_type": "alert"})
63+
64+
4665
class AlertMuteResource(BaseResource):
4766
def post(self, alert_id):
4867
alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org)

redash/handlers/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from werkzeug.wrappers import Response
44

55
from redash.handlers.alerts import (
6+
AlertEvaluateResource,
67
AlertListResource,
78
AlertMuteResource,
89
AlertResource,
@@ -117,6 +118,7 @@ def json_representation(data, code, headers=None):
117118

118119
api.add_org_resource(AlertResource, "/api/alerts/<alert_id>", endpoint="alert")
119120
api.add_org_resource(AlertMuteResource, "/api/alerts/<alert_id>/mute", endpoint="alert_mute")
121+
api.add_org_resource(AlertEvaluateResource, "/api/alerts/<alert_id>/eval", endpoint="alert_eval")
120122
api.add_org_resource(
121123
AlertSubscriptionListResource,
122124
"/api/alerts/<alert_id>/subscriptions",

tests/handlers/test_alerts.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
import datetime
2+
3+
from mock import patch
4+
15
from redash.models import Alert, AlertSubscription, db
6+
from redash.utils import utcnow
27
from tests import BaseTestCase
38

49

@@ -39,6 +44,26 @@ def test_updates_alert(self):
3944
self.assertEqual(rv.status_code, 200)
4045

4146

47+
class TestAlertEvaluateResource(BaseTestCase):
48+
@patch("redash.handlers.alerts.notify_subscriptions")
49+
def test_evaluates_alert_and_notifies(self, mock_notify_subscriptions):
50+
query = self.factory.create_query(
51+
data_source=self.factory.create_data_source(group=self.factory.create_group())
52+
)
53+
retrieved_at = utcnow() - datetime.timedelta(days=1)
54+
query_result = self.factory.create_query_result(
55+
retrieved_at=retrieved_at,
56+
query_text=query.query_text,
57+
query_hash=query.query_hash,
58+
)
59+
query.latest_query_data = query_result
60+
alert = self.factory.create_alert(query_rel=query)
61+
rv = self.make_request("post", "/api/alerts/{}/eval".format(alert.id))
62+
63+
self.assertEqual(rv.status_code, 200)
64+
mock_notify_subscriptions.assert_called()
65+
66+
4267
class TestAlertResourceDelete(BaseTestCase):
4368
def test_removes_alert_and_subscriptions(self):
4469
subscription = self.factory.create_alert_subscription()

0 commit comments

Comments
 (0)