Bladeren bron

Merge branch 'alert_handles' into alert_ui_take2

Conflicts:
	pkg/services/alerting/commands.go
Torkel Ödegaard 9 jaren geleden
bovenliggende
commit
9216492d55

+ 6 - 2
pkg/services/alerting/alert_rule.go

@@ -26,6 +26,10 @@ type AlertRule struct {
 	Transformer     transformer.Transformer
 }
 
+func getTimeDurationStringToSeconds(str string) int64 {
+	return 60
+}
+
 func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) {
 	model := &AlertRule{}
 	model.Id = ruleDef.Id
@@ -40,13 +44,13 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) {
 		Level:    critical.Get("level").MustFloat64(),
 	}
 
-	warning := ruleDef.Expression.Get("warning")
+	warning := ruleDef.Expression.Get("warn")
 	model.Warning = Level{
 		Operator: warning.Get("op").MustString(),
 		Level:    warning.Get("level").MustFloat64(),
 	}
 
-	model.Frequency = ruleDef.Expression.Get("frequency").MustInt64()
+	model.Frequency = getTimeDurationStringToSeconds(ruleDef.Expression.Get("frequency").MustString())
 	model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
 	model.TransformParams = *ruleDef.Expression.Get("transform")
 

+ 0 - 56
pkg/services/alerting/commands.go

@@ -1,11 +1,8 @@
 package alerting
 
 import (
-	"fmt"
-
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/alerting/transformers"
 )
 
 type UpdateDashboardAlertsCommand struct {
@@ -39,56 +36,3 @@ func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
 
 	return nil
 }
-
-func getTimeDurationStringToSeconds(str string) int64 {
-	return 60
-}
-
-func ConvetAlertModelToAlertRule(ruleDef *m.Alert) (*AlertRule, error) {
-	model := &AlertRule{}
-	model.Id = ruleDef.Id
-	model.OrgId = ruleDef.OrgId
-	model.Name = ruleDef.Name
-	model.Description = ruleDef.Description
-	model.State = ruleDef.State
-
-	critical := ruleDef.Expression.Get("critical")
-	model.Critical = Level{
-		Operator: critical.Get("op").MustString(),
-		Level:    critical.Get("level").MustFloat64(),
-	}
-
-	warning := ruleDef.Expression.Get("warning")
-	model.Warning = Level{
-		Operator: warning.Get("op").MustString(),
-		Level:    warning.Get("level").MustFloat64(),
-	}
-
-	model.Frequency = getTimeDurationStringToSeconds(ruleDef.Expression.Get("frequency").MustString())
-	model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
-	model.TransformParams = *ruleDef.Expression.Get("transform")
-
-	if model.Transform == "aggregation" {
-		method := ruleDef.Expression.Get("transform").Get("method").MustString()
-		model.Transformer = transformer.NewAggregationTransformer(method)
-	}
-
-	query := ruleDef.Expression.Get("query")
-	model.Query = AlertQuery{
-		Query:        query.Get("query").MustString(),
-		DatasourceId: query.Get("datasourceId").MustInt64(),
-		From:         query.Get("from").MustString(),
-		To:           query.Get("to").MustString(),
-		Aggregator:   query.Get("agg").MustString(),
-	}
-
-	if model.Query.Query == "" {
-		return nil, fmt.Errorf("missing query.query")
-	}
-
-	if model.Query.DatasourceId == 0 {
-		return nil, fmt.Errorf("missing query.datasourceId")
-	}
-
-	return model, nil
-}

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

@@ -57,12 +57,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
 
 		for _, panelObj := range row.Get("panels").MustArray() {
 			panel := simplejson.NewFromAny(panelObj)
-			jsonAlert := panel.Get("alert")
+			jsonAlert, hasAlert := panel.CheckGet("alert")
 
-			// check if marked for deletion
-			deleted := jsonAlert.Get("deleted").MustBool()
-			if deleted {
-				e.log.Info("Deleted alert rule found")
+			if !hasAlert {
 				continue
 			}
 

+ 4 - 7
pkg/services/alerting/extractor_test.go

@@ -55,7 +55,7 @@ func TestAlertRuleExtraction(t *testing.T) {
               "method": "avg",
               "type": "aggregation"
             },
-            "warning": {
+            "warn": {
               "level": 10,
               "op": ">"
             }
@@ -90,7 +90,7 @@ func TestAlertRuleExtraction(t *testing.T) {
               "method": "avg",
               "name": "aggregation"
             },
-            "warning": {
+            "warn": {
               "level": 10,
               "op": ">"
             }
@@ -149,10 +149,7 @@ func TestAlertRuleExtraction(t *testing.T) {
           ],
           "title": "Broken influxdb panel",
           "transform": "table",
-          "type": "table",
-					"alert": {
-						"deleted": true
-					}
+          "type": "table"
         }
       ],
       "title": "New row"
@@ -185,7 +182,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 				return nil
 			})
 
-			alerts, err := extractor.GetRuleModels()
+			alerts, err := extractor.GetAlerts()
 
 			Convey("Get rules without error", func() {
 				So(err, ShouldBeNil)

+ 1 - 1
pkg/services/alerting/reader.go

@@ -49,7 +49,7 @@ func (arr *AlertRuleReader) Fetch() []*AlertRule {
 
 	res := make([]*AlertRule, len(cmd.Result))
 	for i, ruleDef := range cmd.Result {
-		model, _ := ConvetAlertModelToAlertRule(ruleDef)
+		model, _ := NewAlertRuleFromDBModel(ruleDef)
 		res[i] = model
 	}
 

+ 0 - 55
pkg/services/sqlstore/alert_rule_parser_test.go

@@ -1,55 +0,0 @@
-package sqlstore
-
-import (
-	"testing"
-
-	"github.com/grafana/grafana/pkg/components/simplejson"
-	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/alerting"
-	. "github.com/smartystreets/goconvey/convey"
-)
-
-func TestAlertRuleModelParsing(t *testing.T) {
-
-	Convey("Parsing alertRule from expression", t, func() {
-		alertRuleDAO := &m.Alert{}
-		json, _ := simplejson.NewJson([]byte(`
-      {
-        "frequency": 10,
-        "warning": {
-          "op": ">",
-          "level": 10
-        },
-        "critical": {
-          "op": ">",
-          "level": 20
-        },
-        "query": {
-          "refId": "A",
-          "from": "5m",
-          "to": "now",
-          "datasourceId": 1,
-          "query": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
-        },
-        "transform": {
-          "type": "aggregation",
-          "method": "avg"
-        }
-			}`))
-
-		alertRuleDAO.Name = "Test"
-		alertRuleDAO.Expression = json
-		rule, _ := alerting.ConvetAlertModelToAlertRule(alertRuleDAO)
-
-		Convey("Confirm that all properties are set", func() {
-			So(rule.Query.Query, ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)")
-			So(rule.Query.From, ShouldEqual, "5m")
-			So(rule.Query.To, ShouldEqual, "now")
-			So(rule.Query.DatasourceId, ShouldEqual, 1)
-			So(rule.Warning.Level, ShouldEqual, 10)
-			So(rule.Warning.Operator, ShouldEqual, ">")
-			So(rule.Critical.Level, ShouldEqual, 20)
-			So(rule.Critical.Operator, ShouldEqual, ">")
-		})
-	})
-}

+ 13 - 10
public/app/features/dashboard/viewStateSrv.js

@@ -120,25 +120,28 @@ function (angular, _, $) {
       if (this.panelScopes.length === 0) { return; }
 
       if (this.dashboard.meta.fullscreen) {
-        if (this.fullscreenPanel) {
-          this.leaveFullscreen(false);
-        }
         var panelScope = this.getPanelScope(this.state.panelId);
-        // panel could be about to be created/added and scope does
-        // not exist yet
         if (!panelScope) {
           return;
         }
 
+        if (this.fullscreenPanel) {
+          // if already fullscreen
+          if (this.fullscreenPanel === panelScope) {
+            return;
+          } else {
+            this.leaveFullscreen(false);
+          }
+        }
+
         if (!panelScope.ctrl.editModeInitiated) {
           panelScope.ctrl.initEditMode();
         }
 
-        this.enterFullscreen(panelScope);
-        return;
-      }
-
-      if (this.fullscreenPanel) {
+        if (!panelScope.ctrl.fullscreen) {
+          this.enterFullscreen(panelScope);
+        }
+      } else if (this.fullscreenPanel) {
         this.leaveFullscreen(true);
       }
     };

+ 2 - 2
public/app/features/panel/panel_ctrl.ts

@@ -152,8 +152,8 @@ export class PanelCtrl {
   calculatePanelHeight() {
     if (this.fullscreen) {
       var docHeight = $(window).height();
-      var editHeight = Math.floor(docHeight * 0.3);
-      var fullscreenHeight = Math.floor(docHeight * 0.7);
+      var editHeight = Math.floor(docHeight * 0.4);
+      var fullscreenHeight = Math.floor(docHeight * 0.6);
       this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
     } else {
       this.containerHeight = this.panel.height || this.row.height;

+ 135 - 0
public/app/plugins/panel/graph/alert_handle.ts

@@ -0,0 +1,135 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import 'jquery.flot';
+import $ from 'jquery';
+import _ from 'lodash';
+
+export class AlertHandleManager {
+  plot: any;
+  placeholder: any;
+  height: any;
+  alert: any;
+
+  constructor(private panelCtrl) {
+    this.alert = panelCtrl.panel.alert;
+  }
+
+  getHandleInnerHtml(type, op, value) {
+    if (op === '>') { op = '&gt;'; }
+    if (op === '<') { op = '&lt;'; }
+
+    return `
+    <div class="alert-handle-line">
+    </div>
+    <div class="alert-handle">
+    <i class="icon-gf icon-gf-${type} alert-icon-${type}"></i>
+    ${op} ${value}
+    </div>`;
+  }
+
+  getFullHandleHtml(type, op, value) {
+    var innerTemplate = this.getHandleInnerHtml(type, op, value);
+    return `
+    <div class="alert-handle-wrapper alert-handle-wrapper--${type}">
+    ${innerTemplate}
+    </div>
+    `;
+  }
+
+  setupDragging(handleElem, levelModel) {
+    var isMoving = false;
+    var lastY = null;
+    var posTop;
+    var plot = this.plot;
+    var panelCtrl = this.panelCtrl;
+
+    function dragging(evt) {
+      if (lastY === null) {
+        lastY = evt.clientY;
+      } else {
+        var diff = evt.clientY - lastY;
+        posTop = posTop + diff;
+        lastY = evt.clientY;
+        handleElem.css({top: posTop + diff});
+      }
+    }
+
+    function stopped() {
+      isMoving = false;
+      // calculate graph level
+      var graphLevel = plot.c2p({left: 0, top: posTop}).y;
+      console.log('canvasPos:' + posTop + ' Graph level: ' + graphLevel);
+      graphLevel = parseInt(graphLevel.toFixed(0));
+      levelModel.level = graphLevel;
+      console.log(levelModel);
+
+      var levelCanvasPos = plot.p2c({x: 0, y: graphLevel});
+      console.log('canvas pos', levelCanvasPos);
+
+      console.log('stopped');
+      handleElem.off("mousemove", dragging);
+      handleElem.off("mouseup", dragging);
+
+      // trigger digest and render
+      panelCtrl.$scope.$apply(function() {
+        panelCtrl.render();
+      });
+    }
+
+    handleElem.bind('mousedown', function() {
+      isMoving = true;
+      lastY = null;
+      posTop = handleElem.position().top;
+      console.log('start pos', posTop);
+
+      handleElem.on("mousemove", dragging);
+      handleElem.on("mouseup", stopped);
+    });
+  }
+
+  cleanUp() {
+    if (this.placeholder) {
+      this.placeholder.find(".alert-handle-wrapper").remove();
+    }
+  }
+
+  renderHandle(type, model, defaultHandleTopPos) {
+    var handleElem = this.placeholder.find(`.alert-handle-wrapper--${type}`);
+    var level = model.level;
+    var levelStr = level;
+    var handleTopPos = 0;
+
+    // handle no value
+    if (!_.isNumber(level)) {
+      levelStr = '';
+      handleTopPos = defaultHandleTopPos;
+    } else {
+      var levelCanvasPos = this.plot.p2c({x: 0, y: level});
+      handleTopPos = Math.min(Math.max(levelCanvasPos.top, 0), this.height) - 6;
+    }
+
+    if (handleElem.length === 0) {
+      console.log('creating handle');
+      handleElem = $(this.getFullHandleHtml(type, model.op, levelStr));
+      this.placeholder.append(handleElem);
+      this.setupDragging(handleElem, model);
+    } else {
+      console.log('reusing handle!');
+      handleElem.html(this.getHandleInnerHtml(type, model.op, levelStr));
+    }
+
+    handleElem.toggleClass('alert-handle-wrapper--no-value', levelStr === '');
+    handleElem.css({top: handleTopPos});
+  }
+
+  draw(plot) {
+    this.plot = plot;
+    this.placeholder = plot.getPlaceholder();
+    this.height = plot.height();
+
+    this.renderHandle('critical', this.alert.critical, 10);
+    this.renderHandle('warn', this.alert.warn, this.height-30);
+  }
+
+}
+

+ 37 - 25
public/app/plugins/panel/graph/alert_tab_ctrl.ts

@@ -50,7 +50,7 @@ export class AlertTabCtrl {
     notify: [],
     enabled: false,
     scheduler: 1,
-    warning: { op: '>', level: undefined },
+    warn: { op: '>', level: undefined },
     critical: { op: '>', level: undefined },
     query: {
       refId: 'A',
@@ -70,12 +70,21 @@ export class AlertTabCtrl {
     $scope.ctrl = this;
 
     this.metricTargets = this.panel.targets.map(val => val);
-
     this.initAlertModel();
+
+    // set panel alert edit mode
+    $scope.$on("$destroy", () => {
+      this.panelCtrl.editingAlert = false;
+      this.panelCtrl.render();
+    });
   }
 
   initAlertModel() {
-    this.alert = this.panel.alert = this.panel.alert || {};
+    if (!this.panel.alert) {
+      return;
+    }
+
+    this.alert = this.panel.alert;
 
     // set defaults
     _.defaults(this.alert, this.defaultValues);
@@ -97,6 +106,9 @@ export class AlertTabCtrl {
     this.query = new QueryPart(this.queryParams, alertQueryDef);
     this.convertThresholdsToAlertThresholds();
     this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type});
+
+    this.panelCtrl.editingAlert = true;
+    this.panelCtrl.render();
   }
 
   queryUpdated() {
@@ -125,36 +137,36 @@ export class AlertTabCtrl {
   }
 
   convertThresholdsToAlertThresholds() {
-    if (this.panel.grid
-        && this.panel.grid.threshold1
-        && this.alert.warnLevel === undefined
-       ) {
-      this.alert.warning.op = '>';
-      this.alert.warning.level = this.panel.grid.threshold1;
-    }
-
-    if (this.panel.grid
-        && this.panel.grid.threshold2
-        && this.alert.critical.level === undefined
-       ) {
-      this.alert.critical.op = '>';
-      this.alert.critical.level = this.panel.grid.threshold2;
-    }
+    // if (this.panel.grid
+    //     && this.panel.grid.threshold1
+    //     && this.alert.warnLevel === undefined
+    //    ) {
+    //   this.alert.warning.op = '>';
+    //   this.alert.warning.level = this.panel.grid.threshold1;
+    // }
+    //
+    // if (this.panel.grid
+    //     && this.panel.grid.threshold2
+    //     && this.alert.critical.level === undefined
+    //    ) {
+    //   this.alert.critical.op = '>';
+    //   this.alert.critical.level = this.panel.grid.threshold2;
+    // }
   }
 
   delete() {
-    this.alert = this.panel.alert = {};
-    this.alert.deleted = true;
-    this.initAlertModel();
+    delete this.panel.alert;
+    this.panelCtrl.editingAlert = false;
+    this.panelCtrl.render();
   }
 
   enable() {
-    delete this.alert.deleted;
-    this.alert.enabled = true;
+    this.panel.alert = {};
+    this.initAlertModel();
   }
 
-  disable() {
-    this.alert.enabled = false;
+  levelsUpdated() {
+    this.panelCtrl.render();
   }
 }
 

+ 67 - 1
public/app/plugins/panel/graph/graph.js

@@ -5,6 +5,7 @@ define([
   'lodash',
   'app/core/utils/kbn',
   './graph_tooltip',
+  './alert_handle',
   'jquery.flot',
   'jquery.flot.selection',
   'jquery.flot.time',
@@ -14,13 +15,16 @@ define([
   'jquery.flot.crosshair',
   './jquery.flot.events',
 ],
-function (angular, $, moment, _, kbn, GraphTooltip) {
+function (angular, $, moment, _, kbn, GraphTooltip, AlertHandle) {
   'use strict';
 
   var module = angular.module('grafana.directives');
   var labelWidthCache = {};
   var panelWidthCache = {};
 
+  // systemjs export
+  var AlertHandleManager = AlertHandle.AlertHandleManager;
+
   module.directive('grafanaGraph', function($rootScope, timeSrv) {
     return {
       restrict: 'A',
@@ -34,6 +38,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
         var legendSideLastValue = null;
         var rootScope = scope.$root;
         var panelWidth = 0;
+        var alertHandles;
 
         rootScope.onAppEvent('setCrosshair', function(event, info) {
           // do not need to to this if event is from this panel
@@ -161,6 +166,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
 
             rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
           }
+
+          if (alertHandles) {
+            alertHandles.draw(plot);
+          }
         }
 
         function processOffsetHook(plot, gridMargin) {
@@ -181,6 +190,18 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
             return;
           }
 
+          // give space to alert editing
+          if (ctrl.editingAlert) {
+            if (!alertHandles) {
+              elem.css('margin-right', '220px');
+              alertHandles = new AlertHandleManager(ctrl);
+            }
+          } else if (alertHandles) {
+            elem.css('margin-right', '0');
+            alertHandles.cleanUp();
+            alertHandles = null;
+          }
+
           var stack = panel.stack ? true : null;
 
           // Populate element
@@ -259,6 +280,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
 
           function callPlot(incrementRenderCounter) {
             try {
+              console.log('rendering');
               $.plot(elem, sortedSeries, options);
             } catch (e) {
               console.log('flotcharts error', e);
@@ -311,6 +333,50 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
         }
 
         function addGridThresholds(options, panel) {
+          if (panel.alert) {
+            var crit = panel.alert.critical;
+            var warn = panel.alert.warn;
+            var critEdge = Infinity;
+            var warnEdge = crit.level;
+
+            if (_.isNumber(crit.level)) {
+              if (crit.op === '<') {
+                critEdge = -Infinity;
+              }
+
+              // fill
+              options.grid.markings.push({
+                yaxis: {from: crit.level, to: critEdge},
+                color: 'rgba(234, 112, 112, 0.10)',
+              });
+
+              // line
+              options.grid.markings.push({
+                yaxis: {from: crit.level, to: crit.level},
+                color: '#ed2e18'
+              });
+            }
+
+            if (_.isNumber(warn.level)) {
+              // if (warn.op === '<') {
+              // }
+
+              // fill
+              options.grid.markings.push({
+                yaxis: {from: warn.level, to: warnEdge},
+                color: 'rgba(216, 200, 27, 0.10)',
+              });
+
+              // line
+              options.grid.markings.push({
+                yaxis: {from: warn.level, to: warn.level},
+                color: '#F79520'
+              });
+            }
+
+            return;
+          }
+
           if (_.isNumber(panel.grid.threshold1)) {
             var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null);
             options.grid.markings.push({

+ 94 - 90
public/app/plugins/panel/graph/partials/tab_alerting.html

@@ -1,116 +1,120 @@
-<div class="editor-row">
-  <div class="gf-form-group section" >
-    <h5 class="section-heading">Alert Query</h5>
-    <div class="gf-form-inline">
-      <div class="gf-form">
-        <query-part-editor
-                    class="gf-form-label query-part"
-                    part="ctrl.query"
-                    part-updated="ctrl.queryUpdated()">
-        </query-part-editor>
-      </div>
-      <div class="gf-form">
-        <span class="gf-form-label">Transform using</span>
-        <div class="gf-form-select-wrapper">
-          <select   class="gf-form-input"
-                    ng-model="ctrl.alert.transform.type"
-                    ng-options="f.type as f.text for f in ctrl.transforms"
-                    ng-change="ctrl.transformChanged()"
-                    >
-          </select>
+
+<div ng-if="ctrl.panel.alert">
+  <div class="editor-row">
+    <div class="gf-form-group section" >
+      <h5 class="section-heading">Alert Query</h5>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <query-part-editor
+             class="gf-form-label query-part"
+             part="ctrl.query"
+             part-updated="ctrl.queryUpdated()">
+          </query-part-editor>
         </div>
-      </div>
-      <div class="gf-form" ng-if="ctrl.transformDef.type === 'aggregation'">
-        <span class="gf-form-label">Method</span>
-        <div class="gf-form-select-wrapper">
-          <select   class="gf-form-input"
-                    ng-model="ctrl.alert.transform.method"
-                    ng-options="f for f in ctrl.aggregators">
-          </select>
+        <div class="gf-form">
+          <span class="gf-form-label">Transform using</span>
+          <div class="gf-form-select-wrapper">
+            <select   class="gf-form-input"
+                      ng-model="ctrl.alert.transform.type"
+                      ng-options="f.type as f.text for f in ctrl.transforms"
+                      ng-change="ctrl.transformChanged()"
+                      >
+            </select>
+          </div>
+        </div>
+        <div class="gf-form" ng-if="ctrl.transformDef.type === 'aggregation'">
+          <span class="gf-form-label">Method</span>
+          <div class="gf-form-select-wrapper">
+            <select   class="gf-form-input"
+                      ng-model="ctrl.alert.transform.method"
+                      ng-options="f for f in ctrl.aggregators">
+            </select>
+          </div>
+        </div>
+        <div class="gf-form" ng-if="ctrl.transformDef.type === 'forecast'">
+          <span class="gf-form-label">Timespan</span>
+          <input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
         </div>
-      </div>
-      <div class="gf-form" ng-if="ctrl.transformDef.type === 'forecast'">
-        <span class="gf-form-label">Timespan</span>
-        <input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
       </div>
     </div>
-  </div>
 
-  <div class="gf-form-group section">
-    <h5 class="section-heading">Levels</h5>
-    <div class="gf-form-inline">
-      <div class="gf-form">
-        <span class="gf-form-label">
-          <i class="icon-gf icon-gf-warn alert-icon-warn"></i>
-          Warn if
-        </span>
-        <metric-segment-model property="ctrl.alert.warning.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
-        <input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.warnLevel" ng-change="ctrl.thresholdsUpdated()"></input>
-      </div>
-      <div class="gf-form">
-        <span class="gf-form-label">
-          <i class="icon-gf icon-gf-warn alert-icon-critical"></i>
-          Critcal if
-        </span>
-        <metric-segment-model property="ctrl.alert.critical.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
-        <input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.critLevel" ng-change="ctrl.thresholdsUpdated()"></input>
+    <div class="gf-form-group section">
+      <h5 class="section-heading">Levels</h5>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <span class="gf-form-label">
+            <i class="icon-gf icon-gf-warn alert-icon-warn"></i>
+            Warn if
+          </span>
+          <metric-segment-model property="ctrl.alert.warn.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
+          <input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.warn.level" ng-change="ctrl.levelsUpdated()"></input>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label">
+            <i class="icon-gf icon-gf-warn alert-icon-critical"></i>
+            Critcal if
+          </span>
+          <metric-segment-model property="ctrl.alert.critical.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
+          <input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.critical.level" ng-change="ctrl.levelsUpdated()"></input>
+        </div>
       </div>
     </div>
   </div>
-</div>
 
-<div class="editor-row">
-  <div class="gf-form-group section">
-    <h5 class="section-heading">Execution</h5>
-    <div class="gf-form-inline">
-      <div class="gf-form">
-        <span class="gf-form-label">Scheduler</span>
-        <div class="gf-form-select-wrapper">
-          <select   class="gf-form-input"
-                    ng-model="ctrl.alert.scheduler"
-                    ng-options="f.value as f.text for f in ctrl.schedulers">
-          </select>
+  <div class="editor-row">
+    <div class="gf-form-group section">
+      <h5 class="section-heading">Execution</h5>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <span class="gf-form-label">Scheduler</span>
+          <div class="gf-form-select-wrapper">
+            <select   class="gf-form-input"
+                      ng-model="ctrl.alert.scheduler"
+                      ng-options="f.value as f.text for f in ctrl.schedulers">
+            </select>
+          </div>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label">Evaluate every</span>
+          <input class="gf-form-input max-width-7" type="text" ng-model="ctrl.alert.frequency"></input>
         </div>
-      </div>
-      <div class="gf-form">
-        <span class="gf-form-label">Evaluate every</span>
-        <input class="gf-form-input max-width-7" type="text" ng-model="ctrl.alert.frequency"></input>
       </div>
     </div>
-  </div>
-  <div class="gf-form-group section">
-    <h5 class="section-heading">Notifications</h5>
-    <div class="gf-form-inline">
-      <div class="gf-form">
-        <span class="gf-form-label">Groups</span>
-        <bootstrap-tagsinput ng-model="ctrl.alert.notify" tagclass="label label-tag" placeholder="add tags">
-				</bootstrap-tagsinput>
+    <div class="gf-form-group section">
+      <h5 class="section-heading">Notifications</h5>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <span class="gf-form-label">Groups</span>
+          <bootstrap-tagsinput ng-model="ctrl.alert.notify" tagclass="label label-tag" placeholder="add tags">
+          </bootstrap-tagsinput>
+        </div>
       </div>
     </div>
   </div>
-</div>
-
 
-<div class="gf-form-group section">
-  <h5 class="section-heading">Information</h5>
-  <div class="gf-form">
-    <span class="gf-form-label width-10">Alert name</span>
-    <input type="text" class="gf-form-input width-22" ng-model="ctrl.panel.alerting.name">
-  </div>
-  <div class="gf-form-inline">
+  <div class="gf-form-group section">
+    <h5 class="section-heading">Information</h5>
     <div class="gf-form">
-      <span class="gf-form-label width-10" style="margin-top: -73px;">Alert description</span>
+      <span class="gf-form-label width-10">Alert name</span>
+      <input type="text" class="gf-form-input width-22" ng-model="ctrl.panel.alerting.name">
     </div>
-    <div class="gf-form">
-      <textarea rows="5" ng-model="ctrl.panel.alerting.description" class="gf-form-input width-22"></textarea>
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <span class="gf-form-label width-10" style="margin-top: -73px;">Alert description</span>
+      </div>
+      <div class="gf-form">
+        <textarea rows="5" ng-model="ctrl.panel.alerting.description" class="gf-form-input width-22"></textarea>
+      </div>
     </div>
   </div>
 </div>
 
 <div class="editor-row">
   <div class="gf-form-button-row">
-    <button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.alert.enabled">Delete</button>
-    <button class="btn btn-success" ng-click="ctrl.enable()" ng-hide="ctrl.alert.enabled">Enable</button>
-    <button class="btn btn-secondary" ng-click="ctrl.disable()" ng-show="ctrl.alert.enabled">Disable</button>
+    <button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.panel.alert">Delete</button>
+    <button class="btn btn-inverse" ng-click="ctrl.enable()" ng-hide="ctrl.panel.alert">
+      <i class="icon-gf icon-gf-alert"></i>
+      Add Alert
+    </button>
   </div>
 </div>

+ 1 - 1
public/sass/_variables.dark.scss

@@ -44,7 +44,7 @@ $brand-text-highlight:  #f7941d;
 // Status colors
 // -------------------------
 $online:                #10a345;
-$warn:                  #ffc03c;
+$warn:                  #F79520;
 $critical:              #ed2e18;
 
 // Scaffolding

+ 64 - 0
public/sass/components/_panel_graph.scss

@@ -315,3 +315,67 @@
   font-size: 12px;
 }
 
+.alert-handle-wrapper {
+  position: absolute;
+  user-select: none;
+
+  .alert-handle {
+    z-index: 10;
+    position: relative;
+    float: right;
+    padding: 0.4rem 0.6rem 0.4rem 0.4rem;
+    background-color: $btn-inverse-bg;
+    box-shadow: $search-shadow;
+    cursor: row-resize;
+    width: 100px;
+    font-size: $font-size-sm;
+    box-shadow: 4px 4px 3px 0px $body-bg;
+    border-radius: 4px;
+    border-width: 0 1px 1px 0;
+    border-style: solid;
+    border-color: $black;
+    text-align: right;
+    color: $text-muted;
+
+    .icon-gf {
+      font-size: 17px;
+      position: relative;
+      top: 0px;
+      float: left;
+    }
+  }
+
+  .alert-handle-line {
+    float: left;
+    height: 2px;
+    margin-top: 13px;
+    z-index: 0;
+    position: relative;
+  }
+
+  &--warn {
+    right: -222px;
+    width: 238px;
+
+    .alert-handle-line {
+      width: 138px;
+      background-color: $warn;
+    }
+  }
+
+  &--critical {
+    right: -105px;
+    width: 123px;
+
+    .alert-handle-line {
+      width: 23px;
+      background-color: $critical;
+    }
+  }
+
+  &--no-value {
+    .alert-handle-line {
+      display: none;
+    }
+  }
+}

+ 0 - 6
public/sass/pages/_dashboard.scss

@@ -197,12 +197,6 @@ div.flot-text {
   bottom: 0;
 }
 
-.panel-fullscreen {
-  .panel-title-container {
-    padding: 8px;
-  }
-}
-
 .panel-full-edit {
   margin-top: 20px;
   margin-bottom: 20px;

+ 1 - 1
public/vendor/flot/jquery.flot.js

@@ -1322,7 +1322,7 @@ Licensed under the MIT license.
 
             placeholder.css("padding", 0) // padding messes up the positioning
                 .children().filter(function(){
-                    return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base');
+                    return $(this).hasClass("flot-text");
                 }).remove();
 
             if (placeholder.css("position") == 'static')