Skip to content

Commit 5fd78fd

Browse files
ranbenaarikfr
authored andcommittedNov 2, 2019
New feature - Alert muting (#4276)
* New feature - Alert muting * pep8 fix * Fixed backend api update * whoops semicolon * Implemented mute
1 parent 74dbb8a commit 5fd78fd

File tree

11 files changed

+154
-6
lines changed

11 files changed

+154
-6
lines changed
 

‎client/app/pages/alert/Alert.jsx

+38-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class AlertPage extends React.Component {
5555
options: {
5656
op: '>',
5757
value: 1,
58+
muted: false,
5859
},
5960
}),
6061
pendingRearm: 0,
@@ -159,6 +160,30 @@ class AlertPage extends React.Component {
159160
});
160161
};
161162

163+
mute = () => {
164+
const { alert } = this.state;
165+
return alert.$mute()
166+
.then(() => {
167+
this.setAlertOptions({ muted: true });
168+
notification.warn('Notifications have been muted.');
169+
})
170+
.catch(() => {
171+
notification.error('Failed muting notifications.');
172+
});
173+
}
174+
175+
unmute = () => {
176+
const { alert } = this.state;
177+
return alert.$unmute()
178+
.then(() => {
179+
this.setAlertOptions({ muted: false });
180+
notification.success('Notifications have been restored.');
181+
})
182+
.catch(() => {
183+
notification.error('Failed restoring notifications.');
184+
});
185+
}
186+
162187
edit = () => {
163188
const { id } = this.state.alert;
164189
navigateTo(`/alerts/${id}/edit`, true, false);
@@ -177,11 +202,15 @@ class AlertPage extends React.Component {
177202
return <LoadingState className="m-t-30" />;
178203
}
179204

205+
const muted = !!alert.options.muted;
180206
const { queryResult, mode, canEdit, pendingRearm } = this.state;
181207

182208
const menuButton = (
183209
<MenuButton
184210
doDelete={this.delete}
211+
muted={muted}
212+
mute={this.mute}
213+
unmute={this.unmute}
185214
canEdit={canEdit}
186215
/>
187216
);
@@ -202,7 +231,15 @@ class AlertPage extends React.Component {
202231
return (
203232
<div className="container alert-page">
204233
{mode === MODES.NEW && <AlertNew {...commonProps} />}
205-
{mode === MODES.VIEW && <AlertView canEdit={canEdit} onEdit={this.edit} {...commonProps} />}
234+
{mode === MODES.VIEW && (
235+
<AlertView
236+
canEdit={canEdit}
237+
onEdit={this.edit}
238+
muted={muted}
239+
unmute={this.unmute}
240+
{...commonProps}
241+
/>
242+
)}
206243
{mode === MODES.EDIT && <AlertEdit cancel={this.cancel} {...commonProps} />}
207244
</div>
208245
);

‎client/app/pages/alert/AlertView.jsx

+32
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Alert as AlertType } from '@/components/proptypes';
88
import Form from 'antd/lib/form';
99
import Button from 'antd/lib/button';
1010
import Tooltip from 'antd/lib/tooltip';
11+
import AntAlert from 'antd/lib/alert';
1112

1213
import Title from './components/Title';
1314
import Criteria from './components/Criteria';
@@ -47,6 +48,17 @@ AlertState.defaultProps = {
4748

4849
// eslint-disable-next-line react/prefer-stateless-function
4950
export default class AlertView extends React.Component {
51+
state = {
52+
unmuting: false,
53+
}
54+
55+
unmute = () => {
56+
this.setState({ unmuting: true });
57+
this.props.unmute().finally(() => {
58+
this.setState({ unmuting: false });
59+
});
60+
}
61+
5062
render() {
5163
const { alert, queryResult, canEdit, onEdit, menuButton } = this.props;
5264
const { query, name, options, rearm } = alert;
@@ -87,6 +99,24 @@ export default class AlertView extends React.Component {
8799
</Form>
88100
</div>
89101
<div className="col-md-4">
102+
{options.muted && (
103+
<AntAlert
104+
className="m-b-20"
105+
message={<><i className="fa fa-bell-slash-o" /> Notifications are muted</>}
106+
description={(
107+
<>
108+
Notifications for this alert will not be sent.<br />
109+
{canEdit && (
110+
<>
111+
To restore notifications click
112+
<Button size="small" type="primary" onClick={this.unmute} loading={this.state.unmuting} className="m-t-5 m-l-5">Unmute</Button>
113+
</>
114+
)}
115+
</>
116+
)}
117+
type="warning"
118+
/>
119+
)}
90120
<h4>Destinations{' '}
91121
<Tooltip title="Open Alert Destinations page in a new tab.">
92122
<a href="destinations" target="_blank">
@@ -108,8 +138,10 @@ AlertView.propTypes = {
108138
canEdit: PropTypes.bool.isRequired,
109139
onEdit: PropTypes.func.isRequired,
110140
menuButton: PropTypes.node.isRequired,
141+
unmute: PropTypes.func,
111142
};
112143

113144
AlertView.defaultProps = {
114145
queryResult: null,
146+
unmute: null,
115147
};

‎client/app/pages/alert/components/AlertDestinations.less

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.alert-destinations {
2-
ul {
2+
position: relative;
3+
4+
ul {
35
list-style: none;
46
padding: 0;
57
margin-top: 15px;
@@ -34,8 +36,8 @@
3436

3537
.add-button {
3638
position: absolute;
37-
right: 14px;
38-
top: 9px;
39+
right: 0;
40+
top:-33px;
3941
}
4042
}
4143

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

+22-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ import Button from 'antd/lib/button';
99
import Icon from 'antd/lib/icon';
1010

1111

12-
export default function MenuButton({ doDelete, canEdit }) {
12+
export default function MenuButton({ doDelete, canEdit, mute, unmute, muted }) {
1313
const [loading, setLoading] = useState(false);
1414

15+
const execute = useCallback((action) => {
16+
setLoading(true);
17+
action().finally(() => {
18+
setLoading(false);
19+
});
20+
}, []);
21+
1522
const confirmDelete = useCallback(() => {
1623
Modal.confirm({
1724
title: 'Delete Alert',
@@ -36,6 +43,13 @@ export default function MenuButton({ doDelete, canEdit }) {
3643
placement="bottomRight"
3744
overlay={(
3845
<Menu>
46+
<Menu.Item>
47+
{muted ? (
48+
<a onClick={() => execute(unmute)}>Unmute Notifications</a>
49+
) : (
50+
<a onClick={() => execute(mute)}>Mute Notifications</a>
51+
)}
52+
</Menu.Item>
3953
<Menu.Item>
4054
<a onClick={confirmDelete}>Delete Alert</a>
4155
</Menu.Item>
@@ -52,4 +66,11 @@ export default function MenuButton({ doDelete, canEdit }) {
5266
MenuButton.propTypes = {
5367
doDelete: PropTypes.func.isRequired,
5468
canEdit: PropTypes.bool.isRequired,
69+
mute: PropTypes.func.isRequired,
70+
unmute: PropTypes.func.isRequired,
71+
muted: PropTypes.bool,
72+
};
73+
74+
MenuButton.defaultProps = {
75+
muted: false,
5576
};

‎client/app/pages/alerts/AlertsList.jsx

+7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ class AlertsList extends React.Component {
2828
};
2929

3030
listColumns = [
31+
Columns.custom.sortable((text, alert) => (
32+
<i className={`fa fa-bell-${alert.options.muted ? 'slash' : 'o'} p-r-0`} />
33+
), {
34+
title: <i className="fa fa-bell p-r-0" />,
35+
field: 'muted',
36+
width: '1%',
37+
}),
3138
Columns.custom.sortable((text, alert) => (
3239
<div>
3340
<a className="table-main-title" href={'alerts/' + alert.id}>{alert.name}</a>

‎client/app/services/alert.js

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ function AlertService($resource, $http) {
3535
return newData;
3636
}].concat($http.defaults.transformRequest),
3737
},
38+
mute: { method: 'POST', url: 'api/alerts/:id/mute' },
39+
unmute: { method: 'DELETE', url: 'api/alerts/:id/mute' },
3840
};
3941
return $resource('api/alerts/:id', { id: '@id' }, actions);
4042
}

‎redash/handlers/alerts.py

+28
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,34 @@ def delete(self, alert_id):
4747
models.db.session.commit()
4848

4949

50+
class AlertMuteResource(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+
alert.options['muted'] = True
56+
models.db.session.commit()
57+
58+
self.record_event({
59+
'action': 'mute',
60+
'object_id': alert.id,
61+
'object_type': 'alert'
62+
})
63+
64+
def delete(self, alert_id):
65+
alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org)
66+
require_admin_or_owner(alert.user.id)
67+
68+
alert.options['muted'] = False
69+
models.db.session.commit()
70+
71+
self.record_event({
72+
'action': 'unmute',
73+
'object_id': alert.id,
74+
'object_type': 'alert'
75+
})
76+
77+
5078
class AlertListResource(BaseResource):
5179
def post(self):
5280
req = request.get_json(True)

‎redash/handlers/api.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from flask_restful import Api
33
from werkzeug.wrappers import Response
44

5-
from redash.handlers.alerts import (AlertListResource, AlertResource,
5+
from redash.handlers.alerts import (AlertListResource,
6+
AlertResource, AlertMuteResource,
67
AlertSubscriptionListResource,
78
AlertSubscriptionResource)
89
from redash.handlers.base import org_scoped_rule
@@ -75,6 +76,7 @@ def json_representation(data, code, headers=None):
7576

7677

7778
api.add_org_resource(AlertResource, '/api/alerts/<alert_id>', endpoint='alert')
79+
api.add_org_resource(AlertMuteResource, '/api/alerts/<alert_id>/mute', endpoint='alert_mute')
7880
api.add_org_resource(AlertSubscriptionListResource, '/api/alerts/<alert_id>/subscriptions', endpoint='alert_subscriptions')
7981
api.add_org_resource(AlertSubscriptionResource, '/api/alerts/<alert_id>/subscriptions/<subscriber_id>', endpoint='alert_subscription')
8082
api.add_org_resource(AlertListResource, '/api/alerts', endpoint='alerts')

‎redash/models/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,10 @@ def custom_subject(self):
890890
def groups(self):
891891
return self.query_rel.groups
892892

893+
@property
894+
def muted(self):
895+
return self.options.get('muted', False)
896+
893897

894898
def generate_slug(ctx):
895899
slug = utils.slugify(ctx.current_parameters['name'])

‎redash/tasks/alerts.py

+4
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,8 @@ def check_alerts_for_query(query_id):
4646
logger.debug("Skipping notification (previous state was unknown and now it's ok).")
4747
continue
4848

49+
if alert.muted:
50+
logger.debug("Skipping notification (alert muted).")
51+
continue
52+
4953
notify_subscriptions(alert, new_state)

‎tests/tasks/test_alerts.py

+9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ def test_doesnt_notify_when_nothing_changed(self):
2525

2626
self.assertFalse(redash.tasks.alerts.notify_subscriptions.called)
2727

28+
def test_doesnt_notify_when_muted(self):
29+
redash.tasks.alerts.notify_subscriptions = MagicMock()
30+
Alert.evaluate = MagicMock(return_value=Alert.TRIGGERED_STATE)
31+
32+
alert = self.factory.create_alert(options={"muted": True})
33+
check_alerts_for_query(alert.query_id)
34+
35+
self.assertFalse(redash.tasks.alerts.notify_subscriptions.called)
36+
2837

2938
class TestNotifySubscriptions(BaseTestCase):
3039
def test_calls_notify_for_subscribers(self):

0 commit comments

Comments
 (0)
Please sign in to comment.