Skip to content

Commit 7c339f0

Browse files
committed
feat(alerting): show alertin state in panel header, closes grafana#6136
1 parent 2c4524b commit 7c339f0

File tree

19 files changed

+194
-53
lines changed

19 files changed

+194
-53
lines changed

pkg/api/alerting.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ func ValidateOrgAlert(c *middleware.Context) {
2525
}
2626
}
2727

28+
func GetAlertStatesForDashboard(c *middleware.Context) Response {
29+
dashboardId := c.QueryInt64("dashboardId")
30+
31+
if dashboardId == 0 {
32+
return ApiError(400, "Missing query parameter dashboardId", nil)
33+
}
34+
35+
query := models.GetAlertStatesForDashboardQuery{
36+
OrgId: c.OrgId,
37+
DashboardId: c.QueryInt64("dashboardId"),
38+
}
39+
40+
if err := bus.Dispatch(&query); err != nil {
41+
return ApiError(500, "Failed to fetch alert states", err)
42+
}
43+
44+
return Json(200, query.Result)
45+
}
46+
2847
// GET /api/alerts
2948
func GetAlerts(c *middleware.Context) Response {
3049
query := models.GetAlertsQuery{

pkg/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ func Register(r *macaron.Macaron) {
254254
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
255255
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
256256
r.Get("/", wrap(GetAlerts))
257+
r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
257258
})
258259

259260
r.Get("/alert-notifications", wrap(GetAlertNotifications))

pkg/models/alert.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,18 @@ type GetAlertByIdQuery struct {
135135

136136
Result *Alert
137137
}
138+
139+
type GetAlertStatesForDashboardQuery struct {
140+
OrgId int64
141+
DashboardId int64
142+
143+
Result []*AlertStateInfoDTO
144+
}
145+
146+
type AlertStateInfoDTO struct {
147+
Id int64 `json:"id"`
148+
DashboardId int64 `json:"dashboardId"`
149+
PanelId int64 `json:"panelId"`
150+
State AlertStateType `json:"state"`
151+
NewStateDate time.Time `json:"newStateDate"`
152+
}

pkg/services/alerting/extractor.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
7474
continue
7575
}
7676

77+
// backward compatability check, can be removed later
7778
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
78-
79-
if !hasEnabled || !enabled.MustBool() {
79+
if hasEnabled && enabled.MustBool() == false {
8080
continue
8181
}
8282

pkg/services/alerting/extractor_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) {
4242
"name": "name1",
4343
"message": "desc1",
4444
"handler": 1,
45-
"enabled": true,
4645
"frequency": "60s",
4746
"conditions": [
4847
{
@@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) {
6665
"name": "name2",
6766
"message": "desc2",
6867
"handler": 0,
69-
"enabled": true,
7068
"frequency": "60s",
7169
"severity": "warning",
7270
"conditions": [

pkg/services/sqlstore/alert.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func init() {
1717
bus.AddHandler("sql", DeleteAlertById)
1818
bus.AddHandler("sql", GetAllAlertQueryHandler)
1919
bus.AddHandler("sql", SetAlertState)
20+
bus.AddHandler("sql", GetAlertStatesForDashboard)
2021
}
2122

2223
func GetAlertById(query *m.GetAlertByIdQuery) error {
@@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
241242
return nil
242243
})
243244
}
245+
246+
func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error {
247+
var rawSql = `SELECT
248+
id,
249+
dashboard_id,
250+
panel_id,
251+
state,
252+
new_state_date
253+
FROM alert
254+
WHERE org_id = ? AND dashboard_id = ?`
255+
256+
query.Result = make([]*m.AlertStateInfoDTO, 0)
257+
err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
258+
259+
return err
260+
}

public/app/features/alerting/alert_tab_ctrl.ts

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,41 +48,27 @@ export class AlertTabCtrl {
4848
$onInit() {
4949
this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
5050

51-
this.initModel();
52-
this.validateModel();
51+
// subscribe to graph threshold handle changes
52+
var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
53+
this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler);
5354

54-
// set panel alert edit mode
55+
// set panel alert edit mode
5556
this.$scope.$on("$destroy", () => {
57+
this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler);
5658
this.panelCtrl.editingThresholds = false;
5759
this.panelCtrl.render();
5860
});
5961

60-
// subscribe to graph threshold handle changes
61-
this.panelCtrl.events.on('threshold-changed', this.graphThresholdChanged.bind(this));
62-
63-
// build notification model
62+
// build notification model
6463
this.notifications = [];
6564
this.alertNotifications = [];
6665
this.alertHistory = [];
6766

6867
return this.backendSrv.get('/api/alert-notifications').then(res => {
6968
this.notifications = res;
7069

71-
_.each(this.alert.notifications, item => {
72-
var model = _.find(this.notifications, {id: item.id});
73-
if (model) {
74-
model.iconClass = this.getNotificationIcon(model.type);
75-
this.alertNotifications.push(model);
76-
}
77-
});
78-
79-
_.each(this.notifications, item => {
80-
if (item.isDefault) {
81-
item.iconClass = this.getNotificationIcon(item.type);
82-
item.bgColor = "#00678b";
83-
this.alertNotifications.push(item);
84-
}
85-
});
70+
this.initModel();
71+
this.validateModel();
8672
});
8773
}
8874

@@ -143,9 +129,8 @@ export class AlertTabCtrl {
143129
}
144130

145131
initModel() {
146-
var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
147-
148-
if (!this.alert.enabled) {
132+
var alert = this.alert = this.panel.alert;
133+
if (!alert) {
149134
return;
150135
}
151136

@@ -169,6 +154,22 @@ export class AlertTabCtrl {
169154

170155
ThresholdMapper.alertToGraphThresholds(this.panel);
171156

157+
for (let addedNotification of alert.notifications) {
158+
var model = _.find(this.notifications, {id: addedNotification.id});
159+
if (model) {
160+
model.iconClass = this.getNotificationIcon(model.type);
161+
this.alertNotifications.push(model);
162+
}
163+
}
164+
165+
for (let notification of this.notifications) {
166+
if (notification.isDefault) {
167+
notification.iconClass = this.getNotificationIcon(notification.type);
168+
notification.bgColor = "#00678b";
169+
this.alertNotifications.push(notification);
170+
}
171+
}
172+
172173
this.panelCtrl.editingThresholds = true;
173174
this.panelCtrl.render();
174175
}
@@ -193,7 +194,7 @@ export class AlertTabCtrl {
193194
}
194195

195196
validateModel() {
196-
if (!this.alert.enabled) {
197+
if (!this.alert) {
197198
return;
198199
}
199200

@@ -310,17 +311,17 @@ export class AlertTabCtrl {
310311
icon: 'fa-trash',
311312
yesText: 'Delete',
312313
onConfirm: () => {
313-
this.alert = this.panel.alert = {enabled: false};
314+
delete this.panel.alert;
315+
this.alert = null;
314316
this.panel.thresholds = [];
315317
this.conditionModels = [];
316318
this.panelCtrl.render();
317319
}
318320
});
319-
320321
}
321322

322323
enable() {
323-
this.alert.enabled = true;
324+
this.panel.alert = {};
324325
this.initModel();
325326
}
326327

public/app/features/alerting/partials/alert_tab.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert.enabled">
1+
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert">
22
<aside class="edit-sidemenu-aside">
33
<ul class="edit-sidemenu">
44
<li ng-class="{active: ctrl.subTabIndex === 0}">
@@ -151,7 +151,7 @@ <h5 class="section-heading">State history <span class="muted small">(last 50 sta
151151
</div>
152152
</div>
153153

154-
<div class="gf-form-group" ng-if="!ctrl.alert.enabled">
154+
<div class="gf-form-group" ng-if="!ctrl.alert">
155155
<div class="gf-form-button-row">
156156
<button class="btn btn-inverse" ng-click="ctrl.enable()">
157157
<i class="icon-gf icon-gf-alert"></i>

public/app/features/annotations/annotations_srv.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import coreModule from 'app/core/core_module';
99

1010
export class AnnotationsSrv {
1111
globalAnnotationsPromise: any;
12+
alertStatesPromise: any;
1213

1314
/** @ngInject */
1415
constructor(private $rootScope,
@@ -22,14 +23,27 @@ export class AnnotationsSrv {
2223

2324
clearCache() {
2425
this.globalAnnotationsPromise = null;
26+
this.alertStatesPromise = null;
2527
}
2628

2729
getAnnotations(options) {
2830
return this.$q.all([
2931
this.getGlobalAnnotations(options),
30-
this.getPanelAnnotations(options)
31-
]).then(allResults => {
32-
return _.flattenDeep(allResults);
32+
this.getPanelAnnotations(options),
33+
this.getAlertStates(options)
34+
]).then(results => {
35+
36+
// combine the annotations and flatten results
37+
var annotations = _.flattenDeep([results[0], results[1]]);
38+
39+
// look for alert state for this panel
40+
var alertState = _.find(results[2], {panelId: options.panel.id});
41+
42+
return {
43+
annotations: annotations,
44+
alertState: alertState,
45+
};
46+
3347
}).catch(err => {
3448
this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
3549
});
@@ -39,7 +53,7 @@ export class AnnotationsSrv {
3953
var panel = options.panel;
4054
var dashboard = options.dashboard;
4155

42-
if (panel && panel.alert && panel.alert.enabled) {
56+
if (panel && panel.alert) {
4357
return this.backendSrv.get('/api/annotations', {
4458
from: options.range.from.valueOf(),
4559
to: options.range.to.valueOf(),
@@ -54,6 +68,28 @@ export class AnnotationsSrv {
5468
return this.$q.when([]);
5569
}
5670

71+
getAlertStates(options) {
72+
if (!options.dashboard.id) {
73+
return this.$q.when([]);
74+
}
75+
76+
// ignore if no alerts
77+
if (options.panel && !options.panel.alert) {
78+
return this.$q.when([]);
79+
}
80+
81+
if (options.range.raw.to !== 'now') {
82+
return this.$q.when([]);
83+
}
84+
85+
if (this.alertStatesPromise) {
86+
return this.alertStatesPromise;
87+
}
88+
89+
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id});
90+
return this.alertStatesPromise;
91+
}
92+
5793
getGlobalAnnotations(options) {
5894
var dashboard = options.dashboard;
5995

public/app/features/dashboard/dashnav/dashnav.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export class DashNavCtrl {
159159
var confirmText = "";
160160
var text2 = $scope.dashboard.title;
161161
var alerts = $scope.dashboard.rows.reduce((memo, row) => {
162-
memo += row.panels.filter(panel => panel.alert && panel.alert.enabled).length;
162+
memo += row.panels.filter(panel => panel.alert).length;
163163
return memo;
164164
}, 0);
165165

public/app/features/panel/metrics_panel_ctrl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ class MetricsPanelCtrl extends PanelCtrl {
131131
var intervalOverride = this.panel.interval;
132132

133133
// if no panel interval check datasource
134-
if (!intervalOverride && this.datasource && this.datasource.interval) {
134+
if (intervalOverride) {
135+
intervalOverride = this.templateSrv.replace(intervalOverride, this.panel.scopedVars);
136+
} else if (this.datasource && this.datasource.interval) {
135137
intervalOverride = this.datasource.interval;
136138
}
137139

public/app/features/panel/panel_directive.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import $ from 'jquery';
66
var module = angular.module('grafana.directives');
77

88
var panelTemplate = `
9-
<div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
9+
<div class="panel-container">
1010
<div class="panel-header">
1111
<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
1212
<span data-placement="top" bs-tooltip="ctrl.error">
@@ -65,6 +65,26 @@ module.directive('grafanaPanel', function() {
6565
link: function(scope, elem) {
6666
var panelContainer = elem.find('.panel-container');
6767
var ctrl = scope.ctrl;
68+
69+
// the reason for handling these classes this way is for performance
70+
// limit the watchers on panels etc
71+
72+
ctrl.events.on('render', () => {
73+
panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true);
74+
panelContainer.toggleClass('panel-has-alert', ctrl.panel.alert !== undefined);
75+
76+
if (panelContainer.hasClass('panel-has-alert')) {
77+
panelContainer.removeClass('panel-alert-state--ok panel-alert-state--alerting');
78+
}
79+
80+
// set special class for ok, or alerting states
81+
if (ctrl.alertState) {
82+
if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') {
83+
panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state);
84+
}
85+
}
86+
});
87+
6888
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() {
6989
panelContainer.css({minHeight: ctrl.containerHeight});
7090
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);

public/app/features/panel/panel_menu.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ function (angular, $, _, Tether) {
1212
.directive('panelMenu', function($compile, linkSrv) {
1313
var linkTemplate =
1414
'<span class="panel-title drag-handle pointer">' +
15+
'<span class="icon-gf panel-alert-icon"></span>' +
1516
'<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>' +
1617
'<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
1718
'<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>' +

public/app/partials/panelgeneral.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
<span class="gf-form-label width-6">Span</span>
99
<select class="gf-form-input gf-size-auto" ng-model="ctrl.panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
1010
</div>
11-
<div class="gf-form max-width-26">
11+
<div class="gf-form">
1212
<span class="gf-form-label width-8">Height</span>
1313
<input type="text" class="gf-form-input max-width-6" ng-model='ctrl.panel.height' placeholder="100px"></input>
14-
<editor-checkbox text="Transparent" model="ctrl.panel.transparent"></editor-checkbox>
1514
</div>
15+
<gf-form-switch class="gf-form" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
1616
</div>
1717

1818
<div class="gf-form-inline">

0 commit comments

Comments
 (0)