Skip to content

Commit

Permalink
Adding Evaluate button for alerts to test them (#7032)
Browse files Browse the repository at this point in the history
  • Loading branch information
ezraodio1 authored Aug 1, 2024
1 parent fc1e1f7 commit 660d04b
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 3 deletions.
21 changes: 20 additions & 1 deletion client/app/pages/alert/Alert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import MenuButton from "./components/MenuButton";
import AlertView from "./AlertView";
import AlertEdit from "./AlertEdit";
import AlertNew from "./AlertNew";
import notifications from "@/services/notifications";

const MODES = {
NEW: 0,
Expand Down Expand Up @@ -178,6 +179,17 @@ class Alert extends React.Component {
});
};

evaluate = () => {
const { alert } = this.state;
return AlertService.evaluate(alert)
.then(() => {
notification.success("Alert evaluated. Refresh page for updated status.");
})
.catch(() => {
notifications.error("Failed to evaluate alert.");
});
};

mute = () => {
const { alert } = this.state;
return AlertService.mute(alert)
Expand Down Expand Up @@ -224,7 +236,14 @@ class Alert extends React.Component {
const { queryResult, mode, canEdit, pendingRearm } = this.state;

const menuButton = (
<MenuButton doDelete={this.delete} muted={muted} mute={this.mute} unmute={this.unmute} canEdit={canEdit} />
<MenuButton
doDelete={this.delete}
muted={muted}
mute={this.mute}
unmute={this.unmute}
canEdit={canEdit}
evaluate={this.evaluate}
/>
);

const commonProps = {
Expand Down
6 changes: 5 additions & 1 deletion client/app/pages/alert/components/MenuButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import LoadingOutlinedIcon from "@ant-design/icons/LoadingOutlined";
import EllipsisOutlinedIcon from "@ant-design/icons/EllipsisOutlined";
import PlainButton from "@/components/PlainButton";

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

const execute = useCallback(action => {
Expand Down Expand Up @@ -55,6 +55,9 @@ export default function MenuButton({ doDelete, canEdit, mute, unmute, muted }) {
<Menu.Item>
<PlainButton onClick={confirmDelete}>Delete</PlainButton>
</Menu.Item>
<Menu.Item>
<PlainButton onClick={() => execute(evaluate)}>Evaluate</PlainButton>
</Menu.Item>
</Menu>
}>
<Button aria-label="More actions">
Expand All @@ -69,6 +72,7 @@ MenuButton.propTypes = {
canEdit: PropTypes.bool.isRequired,
mute: PropTypes.func.isRequired,
unmute: PropTypes.func.isRequired,
evaluate: PropTypes.func.isRequired,
muted: PropTypes.bool,
};

Expand Down
1 change: 1 addition & 0 deletions client/app/services/alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const Alert = {
delete: data => axios.delete(`api/alerts/${data.id}`),
mute: data => axios.post(`api/alerts/${data.id}/mute`),
unmute: data => axios.delete(`api/alerts/${data.id}/mute`),
evaluate: data => axios.post(`api/alerts/${data.id}/eval`),
};

export default Alert;
21 changes: 20 additions & 1 deletion redash/handlers/alerts.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import request
from funcy import project

from redash import models
from redash import models, utils
from redash.handlers.base import (
BaseResource,
get_object_or_404,
Expand All @@ -14,6 +14,10 @@
view_only,
)
from redash.serializers import serialize_alert
from redash.tasks.alerts import (
notify_subscriptions,
should_notify,
)


class AlertResource(BaseResource):
Expand Down Expand Up @@ -43,6 +47,21 @@ def delete(self, alert_id):
models.db.session.commit()


class AlertEvaluateResource(BaseResource):
def post(self, alert_id):
alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org)
require_admin_or_owner(alert.user.id)

new_state = alert.evaluate()
if should_notify(alert, new_state):
alert.state = new_state
alert.last_triggered_at = utils.utcnow()
models.db.session.commit()

notify_subscriptions(alert, new_state, {})
self.record_event({"action": "evaluate", "object_id": alert.id, "object_type": "alert"})


class AlertMuteResource(BaseResource):
def post(self, alert_id):
alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org)
Expand Down
2 changes: 2 additions & 0 deletions redash/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from werkzeug.wrappers import Response

from redash.handlers.alerts import (
AlertEvaluateResource,
AlertListResource,
AlertMuteResource,
AlertResource,
Expand Down Expand Up @@ -117,6 +118,7 @@ def json_representation(data, code, headers=None):

api.add_org_resource(AlertResource, "/api/alerts/<alert_id>", endpoint="alert")
api.add_org_resource(AlertMuteResource, "/api/alerts/<alert_id>/mute", endpoint="alert_mute")
api.add_org_resource(AlertEvaluateResource, "/api/alerts/<alert_id>/eval", endpoint="alert_eval")
api.add_org_resource(
AlertSubscriptionListResource,
"/api/alerts/<alert_id>/subscriptions",
Expand Down
25 changes: 25 additions & 0 deletions tests/handlers/test_alerts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import datetime

from mock import patch

from redash.models import Alert, AlertSubscription, db
from redash.utils import utcnow
from tests import BaseTestCase


Expand Down Expand Up @@ -39,6 +44,26 @@ def test_updates_alert(self):
self.assertEqual(rv.status_code, 200)


class TestAlertEvaluateResource(BaseTestCase):
@patch("redash.handlers.alerts.notify_subscriptions")
def test_evaluates_alert_and_notifies(self, mock_notify_subscriptions):
query = self.factory.create_query(
data_source=self.factory.create_data_source(group=self.factory.create_group())
)
retrieved_at = utcnow() - datetime.timedelta(days=1)
query_result = self.factory.create_query_result(
retrieved_at=retrieved_at,
query_text=query.query_text,
query_hash=query.query_hash,
)
query.latest_query_data = query_result
alert = self.factory.create_alert(query_rel=query)
rv = self.make_request("post", "/api/alerts/{}/eval".format(alert.id))

self.assertEqual(rv.status_code, 200)
mock_notify_subscriptions.assert_called()


class TestAlertResourceDelete(BaseTestCase):
def test_removes_alert_and_subscriptions(self):
subscription = self.factory.create_alert_subscription()
Expand Down

0 comments on commit 660d04b

Please sign in to comment.