Ver Fonte

feat(alerting): show alertin state in panel header, closes #6136

Torkel Ödegaard há 9 anos atrás
pai
commit
7c339f0794

+ 19 - 0
pkg/api/alerting.go

@@ -25,6 +25,25 @@ func ValidateOrgAlert(c *middleware.Context) {
 	}
 }
 
+func GetAlertStatesForDashboard(c *middleware.Context) Response {
+	dashboardId := c.QueryInt64("dashboardId")
+
+	if dashboardId == 0 {
+		return ApiError(400, "Missing query parameter dashboardId", nil)
+	}
+
+	query := models.GetAlertStatesForDashboardQuery{
+		OrgId:       c.OrgId,
+		DashboardId: c.QueryInt64("dashboardId"),
+	}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return ApiError(500, "Failed to fetch alert states", err)
+	}
+
+	return Json(200, query.Result)
+}
+
 // GET /api/alerts
 func GetAlerts(c *middleware.Context) Response {
 	query := models.GetAlertsQuery{

+ 1 - 0
pkg/api/api.go

@@ -254,6 +254,7 @@ func Register(r *macaron.Macaron) {
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/", wrap(GetAlerts))
+			r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
 		})
 
 		r.Get("/alert-notifications", wrap(GetAlertNotifications))

+ 15 - 0
pkg/models/alert.go

@@ -135,3 +135,18 @@ type GetAlertByIdQuery struct {
 
 	Result *Alert
 }
+
+type GetAlertStatesForDashboardQuery struct {
+	OrgId       int64
+	DashboardId int64
+
+	Result []*AlertStateInfoDTO
+}
+
+type AlertStateInfoDTO struct {
+	Id           int64          `json:"id"`
+	DashboardId  int64          `json:"dashboardId"`
+	PanelId      int64          `json:"panelId"`
+	State        AlertStateType `json:"state"`
+	NewStateDate time.Time      `json:"newStateDate"`
+}

+ 2 - 2
pkg/services/alerting/extractor.go

@@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
 				continue
 			}
 
+			// backward compatability check, can be removed later
 			enabled, hasEnabled := jsonAlert.CheckGet("enabled")
-
-			if !hasEnabled || !enabled.MustBool() {
+			if hasEnabled && enabled.MustBool() == false {
 				continue
 			}
 

+ 0 - 2
pkg/services/alerting/extractor_test.go

@@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) {
               "name": "name1",
               "message": "desc1",
               "handler": 1,
-              "enabled": true,
               "frequency": "60s",
               "conditions": [
               {
@@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) {
               "name": "name2",
               "message": "desc2",
               "handler": 0,
-              "enabled": true,
               "frequency": "60s",
               "severity": "warning",
               "conditions": [

+ 17 - 0
pkg/services/sqlstore/alert.go

@@ -17,6 +17,7 @@ func init() {
 	bus.AddHandler("sql", DeleteAlertById)
 	bus.AddHandler("sql", GetAllAlertQueryHandler)
 	bus.AddHandler("sql", SetAlertState)
+	bus.AddHandler("sql", GetAlertStatesForDashboard)
 }
 
 func GetAlertById(query *m.GetAlertByIdQuery) error {
@@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
 		return nil
 	})
 }
+
+func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error {
+	var rawSql = `SELECT
+	                id,
+	                dashboard_id,
+	                panel_id,
+	                state,
+	                new_state_date
+	                FROM alert
+	                WHERE org_id = ? AND dashboard_id = ?`
+
+	query.Result = make([]*m.AlertStateInfoDTO, 0)
+	err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
+
+	return err
+}

+ 30 - 29
public/app/features/alerting/alert_tab_ctrl.ts

@@ -48,19 +48,18 @@ export class AlertTabCtrl {
   $onInit() {
     this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
 
-    this.initModel();
-    this.validateModel();
+    // subscribe to graph threshold handle changes
+    var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
+    this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler);
 
-    // set panel alert edit mode
+   // set panel alert edit mode
     this.$scope.$on("$destroy", () => {
+      this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler);
       this.panelCtrl.editingThresholds = false;
       this.panelCtrl.render();
     });
 
-    // subscribe to graph threshold handle changes
-    this.panelCtrl.events.on('threshold-changed', this.graphThresholdChanged.bind(this));
-
-    // build notification model
+       // build notification model
     this.notifications = [];
     this.alertNotifications = [];
     this.alertHistory = [];
@@ -68,21 +67,8 @@ export class AlertTabCtrl {
     return this.backendSrv.get('/api/alert-notifications').then(res => {
       this.notifications = res;
 
-      _.each(this.alert.notifications, item => {
-        var model = _.find(this.notifications, {id: item.id});
-        if (model) {
-          model.iconClass = this.getNotificationIcon(model.type);
-          this.alertNotifications.push(model);
-        }
-      });
-
-      _.each(this.notifications, item => {
-        if (item.isDefault) {
-          item.iconClass = this.getNotificationIcon(item.type);
-          item.bgColor = "#00678b";
-          this.alertNotifications.push(item);
-        }
-      });
+      this.initModel();
+      this.validateModel();
     });
   }
 
@@ -143,9 +129,8 @@ export class AlertTabCtrl {
   }
 
   initModel() {
-    var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
-
-    if (!this.alert.enabled) {
+    var alert = this.alert = this.panel.alert;
+    if (!alert) {
       return;
     }
 
@@ -169,6 +154,22 @@ export class AlertTabCtrl {
 
     ThresholdMapper.alertToGraphThresholds(this.panel);
 
+    for (let addedNotification of alert.notifications) {
+      var model = _.find(this.notifications, {id: addedNotification.id});
+      if (model) {
+        model.iconClass = this.getNotificationIcon(model.type);
+        this.alertNotifications.push(model);
+      }
+    }
+
+    for (let notification of this.notifications) {
+      if (notification.isDefault) {
+        notification.iconClass = this.getNotificationIcon(notification.type);
+        notification.bgColor = "#00678b";
+        this.alertNotifications.push(notification);
+      }
+    }
+
     this.panelCtrl.editingThresholds = true;
     this.panelCtrl.render();
   }
@@ -193,7 +194,7 @@ export class AlertTabCtrl {
   }
 
   validateModel() {
-    if (!this.alert.enabled) {
+    if (!this.alert) {
       return;
     }
 
@@ -310,17 +311,17 @@ export class AlertTabCtrl {
       icon: 'fa-trash',
       yesText: 'Delete',
       onConfirm: () => {
-        this.alert = this.panel.alert = {enabled: false};
+        delete this.panel.alert;
+        this.alert = null;
         this.panel.thresholds = [];
         this.conditionModels = [];
         this.panelCtrl.render();
       }
     });
-
   }
 
   enable() {
-    this.alert.enabled = true;
+    this.panel.alert = {};
     this.initModel();
   }
 

+ 2 - 2
public/app/features/alerting/partials/alert_tab.html

@@ -1,4 +1,4 @@
-<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert.enabled">
+<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert">
 	<aside class="edit-sidemenu-aside">
 		<ul class="edit-sidemenu">
 			<li ng-class="{active: ctrl.subTabIndex === 0}">
@@ -151,7 +151,7 @@
 	</div>
 </div>
 
-<div class="gf-form-group" ng-if="!ctrl.alert.enabled">
+<div class="gf-form-group" ng-if="!ctrl.alert">
 	<div class="gf-form-button-row">
 		<button class="btn btn-inverse" ng-click="ctrl.enable()">
 			<i class="icon-gf icon-gf-alert"></i>

+ 40 - 4
public/app/features/annotations/annotations_srv.ts

@@ -9,6 +9,7 @@ import coreModule from 'app/core/core_module';
 
 export class AnnotationsSrv {
   globalAnnotationsPromise: any;
+  alertStatesPromise: any;
 
   /** @ngInject */
   constructor(private $rootScope,
@@ -22,14 +23,27 @@ export class AnnotationsSrv {
 
   clearCache() {
     this.globalAnnotationsPromise = null;
+    this.alertStatesPromise = null;
   }
 
   getAnnotations(options) {
     return this.$q.all([
       this.getGlobalAnnotations(options),
-      this.getPanelAnnotations(options)
-    ]).then(allResults => {
-      return _.flattenDeep(allResults);
+      this.getPanelAnnotations(options),
+      this.getAlertStates(options)
+    ]).then(results => {
+
+      // combine the annotations and flatten results
+      var annotations = _.flattenDeep([results[0], results[1]]);
+
+      // look for alert state for this panel
+      var alertState = _.find(results[2], {panelId: options.panel.id});
+
+      return {
+        annotations: annotations,
+        alertState: alertState,
+      };
+
     }).catch(err => {
       this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
     });
@@ -39,7 +53,7 @@ export class AnnotationsSrv {
     var panel = options.panel;
     var dashboard = options.dashboard;
 
-    if (panel && panel.alert && panel.alert.enabled) {
+    if (panel && panel.alert) {
       return this.backendSrv.get('/api/annotations', {
         from: options.range.from.valueOf(),
         to: options.range.to.valueOf(),
@@ -54,6 +68,28 @@ export class AnnotationsSrv {
     return this.$q.when([]);
   }
 
+  getAlertStates(options) {
+    if (!options.dashboard.id) {
+      return this.$q.when([]);
+    }
+
+    // ignore if no alerts
+    if (options.panel && !options.panel.alert) {
+      return this.$q.when([]);
+    }
+
+    if (options.range.raw.to !== 'now') {
+      return this.$q.when([]);
+    }
+
+    if (this.alertStatesPromise) {
+      return this.alertStatesPromise;
+    }
+
+    this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id});
+    return this.alertStatesPromise;
+  }
+
   getGlobalAnnotations(options) {
     var dashboard = options.dashboard;
 

+ 1 - 1
public/app/features/dashboard/dashnav/dashnav.ts

@@ -159,7 +159,7 @@ export class DashNavCtrl {
       var confirmText = "";
       var text2 = $scope.dashboard.title;
       var alerts = $scope.dashboard.rows.reduce((memo, row) => {
-        memo += row.panels.filter(panel => panel.alert && panel.alert.enabled).length;
+        memo += row.panels.filter(panel => panel.alert).length;
         return memo;
       }, 0);
 

+ 3 - 1
public/app/features/panel/metrics_panel_ctrl.ts

@@ -131,7 +131,9 @@ class MetricsPanelCtrl extends PanelCtrl {
     var intervalOverride = this.panel.interval;
 
     // if no panel interval check datasource
-    if (!intervalOverride && this.datasource && this.datasource.interval) {
+    if (intervalOverride) {
+      intervalOverride = this.templateSrv.replace(intervalOverride, this.panel.scopedVars);
+    } else if (this.datasource && this.datasource.interval) {
       intervalOverride = this.datasource.interval;
     }
 

+ 21 - 1
public/app/features/panel/panel_directive.ts

@@ -6,7 +6,7 @@ import $ from 'jquery';
 var module = angular.module('grafana.directives');
 
 var panelTemplate = `
-  <div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
+  <div class="panel-container">
     <div class="panel-header">
       <span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
         <span data-placement="top" bs-tooltip="ctrl.error">
@@ -65,6 +65,26 @@ module.directive('grafanaPanel', function() {
     link: function(scope, elem) {
       var panelContainer = elem.find('.panel-container');
       var ctrl = scope.ctrl;
+
+      // the reason for handling these classes this way is for performance
+      // limit the watchers on panels etc
+
+      ctrl.events.on('render', () => {
+        panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true);
+        panelContainer.toggleClass('panel-has-alert', ctrl.panel.alert !== undefined);
+
+        if (panelContainer.hasClass('panel-has-alert')) {
+          panelContainer.removeClass('panel-alert-state--ok panel-alert-state--alerting');
+        }
+
+        // set special class for ok, or alerting states
+        if (ctrl.alertState) {
+          if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') {
+            panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state);
+          }
+        }
+      });
+
       scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() {
         panelContainer.css({minHeight: ctrl.containerHeight});
         elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);

+ 1 - 0
public/app/features/panel/panel_menu.js

@@ -12,6 +12,7 @@ function (angular, $, _, Tether) {
     .directive('panelMenu', function($compile, linkSrv) {
       var linkTemplate =
           '<span class="panel-title drag-handle pointer">' +
+            '<span class="icon-gf panel-alert-icon"></span>' +
             '<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>' +
             '<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
             '<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>' +

+ 2 - 2
public/app/partials/panelgeneral.html

@@ -8,11 +8,11 @@
 			<span class="gf-form-label width-6">Span</span>
 			<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>
 		</div>
-		<div class="gf-form max-width-26">
+		<div class="gf-form">
 			<span class="gf-form-label width-8">Height</span>
 			<input type="text" class="gf-form-input max-width-6" ng-model='ctrl.panel.height' placeholder="100px"></input>
-			<editor-checkbox text="Transparent" model="ctrl.panel.transparent"></editor-checkbox>
 		</div>
+		<gf-form-switch class="gf-form" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
 	</div>
 
 	<div class="gf-form-inline">

+ 1 - 1
public/app/plugins/panel/graph/graph.ts

@@ -62,7 +62,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
         if (!data) {
           return;
         }
-        annotations = data.annotations || annotations;
+        annotations = ctrl.annotations;
         render_panel();
       });
 

+ 7 - 3
public/app/plugins/panel/graph/module.ts

@@ -22,6 +22,9 @@ class GraphCtrl extends MetricsPanelCtrl {
   hiddenSeries: any = {};
   seriesList: any = [];
   dataList: any = [];
+  annotations: any = [];
+  alertState: any;
+
   annotationsPromise: any;
   datapointsCount: number;
   datapointsOutside: boolean;
@@ -167,11 +170,11 @@ class GraphCtrl extends MetricsPanelCtrl {
 
   onDataError(err) {
     this.seriesList = [];
+    this.annotations = [];
     this.render([]);
   }
 
   onDataReceived(dataList) {
-
     this.dataList = dataList;
     this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
 
@@ -186,9 +189,10 @@ class GraphCtrl extends MetricsPanelCtrl {
       }
     }
 
-    this.annotationsPromise.then(annotations => {
+    this.annotationsPromise.then(result => {
       this.loading = false;
-      this.seriesList.annotations = annotations;
+      this.alertState = result.alertState;
+      this.annotations = result.annotations;
       this.render(this.seriesList);
     }, () => {
       this.loading = false;

+ 1 - 1
public/app/plugins/panel/graph/thresholds_form.ts

@@ -13,7 +13,7 @@ export class ThresholdFormCtrl {
   constructor($scope) {
     this.panel = this.panelCtrl.panel;
 
-    if (this.panel.alert && this.panel.alert.enabled) {
+    if (this.panel.alert) {
       this.disabled = true;
     }
 

+ 1 - 4
public/app/plugins/panel/table/editor.html

@@ -34,10 +34,7 @@
 					ng-change="editor.render()"
 					ng-model-onblur>
 			</div>
-			<gf-form-switch class="gf-form" label-class="width-4"
-				label="Scroll"
-				checked="editor.panel.scroll"
-				change="editor.render()"></gf-form-switch>
+			<gf-form-switch class="gf-form" label-class="width-4" label="Scroll" checked="editor.panel.scroll" on-change="editor.render()"></gf-form-switch>
 			<div class="gf-form max-width-17">
 				<label class="gf-form-label width-6">Font size</label>
 				<div class="gf-form-select-wrapper max-width-15">

+ 30 - 0
public/sass/pages/_alerting.scss

@@ -38,3 +38,33 @@
     top: 2px;
   }
 }
+
+.panel-has-alert {
+  .panel-alert-icon:before {
+    content: "\e611";
+    position: relative;
+    top: 1px;
+    left: -3px;
+  }
+}
+
+.panel-alert-state {
+  &--alerting {
+    box-shadow: 0 0 10px $critical;
+
+    .panel-alert-icon:before {
+      color: $critical;
+      content: "\e610";
+    }
+  }
+
+  &--ok {
+    //box-shadow: 0 0 5px rgba(0,200,0,10.8);
+    .panel-alert-icon:before {
+      color: $online;
+      content: "\e610";
+    }
+  }
+}
+
+