Adding Evaluate button for alerts to test them (#7032)

This commit is contained in:
Ezra Odio
2024-08-01 11:02:12 -04:00
committed by GitHub
parent fc1e1f7a01
commit 660d04b0f1
6 changed files with 73 additions and 3 deletions

View File

@@ -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,
@@ -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)
@@ -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 = {

View File

@@ -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 => {
@@ -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">
@@ -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,
};

View File

@@ -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;

View File

@@ -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,
@@ -14,6 +14,10 @@ from redash.permissions import (
view_only,
)
from redash.serializers import serialize_alert
from redash.tasks.alerts import (
notify_subscriptions,
should_notify,
)
class AlertResource(BaseResource):
@@ -43,6 +47,21 @@ class AlertResource(BaseResource):
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)

View File

@@ -3,6 +3,7 @@ from flask_restful import Api
from werkzeug.wrappers import Response
from redash.handlers.alerts import (
AlertEvaluateResource,
AlertListResource,
AlertMuteResource,
AlertResource,
@@ -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",

View File

@@ -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
@@ -39,6 +44,26 @@ class TestAlertResourcePost(BaseTestCase):
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()