浏览代码

feat(alerting): progress on alerting UI and model, refactoring of dashboard parser and tests into extractor component, moved tests from sqlstore to alerting package

Torkel Ödegaard 9 年之前
父节点
当前提交
2b4a9954b1

+ 5 - 7
pkg/api/dashboard.go

@@ -151,15 +151,13 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
 	}
 
 	if setting.AlertingEnabled {
-		saveAlertCommand := m.SaveAlertsCommand{
-			DashboardId: cmd.Result.Id,
-			OrgId:       c.OrgId,
-			UserId:      c.UserId,
-			Alerts:      alerting.ParseAlertsFromDashboard(&cmd),
+		alertCmd := alerting.UpdateDashboardAlertsCommand{
+			OrgId:     c.OrgId,
+			UserId:    c.UserId,
+			Dashboard: cmd.Result,
 		}
 
-		err = bus.Dispatch(&saveAlertCommand)
-		if err != nil {
+		if err := bus.Dispatch(&alertCmd); err != nil {
 			c.JsonApiErr(500, "Failed to save alerts", err)
 			return
 		}

+ 6 - 1
pkg/models/alerts.go

@@ -14,6 +14,9 @@ type AlertRuleModel struct {
 	Name        string
 	Description string
 	State       string
+	Scheduler   int64
+	Enabled     bool
+	Frequency   int
 
 	Created time.Time
 	Updated time.Time
@@ -21,6 +24,8 @@ type AlertRuleModel struct {
 	Expression *simplejson.Json
 }
 
+type AlertRules []*AlertRuleModel
+
 func (this AlertRuleModel) TableName() string {
 	return "alert_rule"
 }
@@ -83,7 +88,7 @@ type SaveAlertsCommand struct {
 	UserId      int64
 	OrgId       int64
 
-	Alerts []*AlertRuleModel
+	Alerts AlertRules
 }
 
 type DeleteAlertCommand struct {

+ 76 - 0
pkg/services/alerting/alert_rule.go

@@ -0,0 +1,76 @@
+package alerting
+
+import (
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+type AlertRule struct {
+	Id              int64
+	OrgId           int64
+	DashboardId     int64
+	PanelId         int64
+	Frequency       int64
+	Name            string
+	Description     string
+	State           string
+	Warning         Level
+	Critical        Level
+	Query           AlertQuery
+	Transform       string
+	TransformParams simplejson.Json
+	Transformer     Transformer
+}
+
+func NewAlertRuleFromDBModel(ruleDef *m.AlertRuleModel) (*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 = ruleDef.Expression.Get("frequency").MustInt64()
+	model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
+	model.TransformParams = *ruleDef.Expression.Get("transform")
+
+	if model.Transform == "aggregation" {
+		model.Transformer = &AggregationTransformer{
+			Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
+		}
+	}
+
+	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
+}

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

@@ -0,0 +1,89 @@
+package alerting
+
+import (
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+type UpdateDashboardAlertsCommand struct {
+	UserId    int64
+	OrgId     int64
+	Dashboard *m.Dashboard
+}
+
+func init() {
+	bus.AddHandler("alerting", updateDashboardAlerts)
+}
+
+func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
+	saveRulesCmd := m.SaveAlertsCommand{
+		OrgId:  cmd.OrgId,
+		UserId: cmd.UserId,
+	}
+
+	extractor := NewAlertRuleExtractor(cmd.Dashboard, cmd.OrgId)
+
+	rules, err := extractor.GetRuleModels()
+	if err != nil {
+		return err
+	}
+
+	saveRulesCmd.Alerts = rules
+	if bus.Dispatch(&saveRulesCmd); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*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 = ruleDef.Expression.Get("frequency").MustInt64()
+	model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
+	model.TransformParams = *ruleDef.Expression.Get("transform")
+
+	if model.Transform == "aggregation" {
+		model.Transformer = &AggregationTransformer{
+			Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
+		}
+	}
+
+	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
+}

+ 0 - 135
pkg/services/alerting/dashboard_parser.go

@@ -1,135 +0,0 @@
-package alerting
-
-import (
-	"fmt"
-
-	"github.com/grafana/grafana/pkg/bus"
-	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/log"
-	m "github.com/grafana/grafana/pkg/models"
-)
-
-func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleModel {
-	alerts := make([]*m.AlertRuleModel, 0)
-
-	for _, rowObj := range cmd.Dashboard.Get("rows").MustArray() {
-		row := simplejson.NewFromAny(rowObj)
-
-		for _, panelObj := range row.Get("panels").MustArray() {
-			panel := simplejson.NewFromAny(panelObj)
-
-			alerting := panel.Get("alerting")
-			alert := &m.AlertRuleModel{
-				DashboardId: cmd.Result.Id,
-				OrgId:       cmd.Result.OrgId,
-				PanelId:     panel.Get("id").MustInt64(),
-				Id:          alerting.Get("id").MustInt64(),
-				Name:        alerting.Get("name").MustString(),
-				Description: alerting.Get("description").MustString(),
-			}
-
-			log.Info("Alertrule: %v", alert.Name)
-
-			valueQuery := alerting.Get("query")
-			valueQueryRef := valueQuery.Get("refId").MustString()
-			for _, targetsObj := range panel.Get("targets").MustArray() {
-				target := simplejson.NewFromAny(targetsObj)
-
-				if target.Get("refId").MustString() == valueQueryRef {
-					datsourceName := ""
-					if target.Get("datasource").MustString() != "" {
-						datsourceName = target.Get("datasource").MustString()
-					} else if panel.Get("datasource").MustString() != "" {
-						datsourceName = panel.Get("datasource").MustString()
-					}
-
-					if datsourceName == "" {
-						query := &m.GetDataSourcesQuery{OrgId: cmd.OrgId}
-						if err := bus.Dispatch(query); err == nil {
-							for _, ds := range query.Result {
-								if ds.IsDefault {
-									alerting.SetPath([]string{"query", "datasourceId"}, ds.Id)
-								}
-							}
-						}
-					} else {
-						query := &m.GetDataSourceByNameQuery{
-							Name:  panel.Get("datasource").MustString(),
-							OrgId: cmd.OrgId,
-						}
-						bus.Dispatch(query)
-						alerting.SetPath([]string{"query", "datasourceId"}, query.Result.Id)
-					}
-
-					targetQuery := target.Get("target").MustString()
-					if targetQuery != "" {
-						alerting.SetPath([]string{"query", "query"}, targetQuery)
-					}
-				}
-			}
-
-			alert.Expression = alerting
-
-			_, err := ConvetAlertModelToAlertRule(alert)
-
-			if err == nil && alert.ValidToSave() {
-				alerts = append(alerts, alert)
-			} else {
-				log.Error2("Failed to parse model from expression", "error", err)
-			}
-
-		}
-	}
-
-	return alerts
-}
-
-func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*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 = ruleDef.Expression.Get("frequency").MustInt64()
-	model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
-	model.TransformParams = *ruleDef.Expression.Get("transform")
-
-	if model.Transform == "aggregation" {
-		model.Transformer = &AggregationTransformer{
-			Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
-		}
-	}
-
-	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
-}

+ 120 - 0
pkg/services/alerting/extractor.go

@@ -0,0 +1,120 @@
+package alerting
+
+import (
+	"errors"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/log"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+type AlertRuleExtractor struct {
+	Dash  *m.Dashboard
+	OrgId int64
+	log   log.Logger
+}
+
+func NewAlertRuleExtractor(dash *m.Dashboard, orgId int64) *AlertRuleExtractor {
+	return &AlertRuleExtractor{
+		Dash:  dash,
+		OrgId: orgId,
+		log:   log.New("alerting.extractor"),
+	}
+}
+
+func (e *AlertRuleExtractor) lookupDatasourceId(dsName string) (int64, error) {
+	if dsName == "" {
+		query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
+		if err := bus.Dispatch(query); err != nil {
+			return 0, err
+		} else {
+			for _, ds := range query.Result {
+				if ds.IsDefault {
+					return ds.Id, nil
+				}
+			}
+		}
+	} else {
+		query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
+		if err := bus.Dispatch(query); err != nil {
+			return 0, err
+		} else {
+			return query.Result.Id, nil
+		}
+	}
+
+	return 0, errors.New("Could not find datasource id for " + dsName)
+}
+
+func (e *AlertRuleExtractor) GetRuleModels() (m.AlertRules, error) {
+
+	rules := make(m.AlertRules, 0)
+
+	for _, rowObj := range e.Dash.Data.Get("rows").MustArray() {
+		row := simplejson.NewFromAny(rowObj)
+
+		for _, panelObj := range row.Get("panels").MustArray() {
+			panel := simplejson.NewFromAny(panelObj)
+			jsonRule := panel.Get("alerting")
+
+			// check if marked for deletion
+			deleted := jsonRule.Get("deleted").MustBool()
+			if deleted {
+				e.log.Info("Deleted alert rule found")
+				continue
+			}
+
+			ruleModel := &m.AlertRuleModel{
+				DashboardId: e.Dash.Id,
+				OrgId:       e.OrgId,
+				PanelId:     panel.Get("id").MustInt64(),
+				Id:          jsonRule.Get("id").MustInt64(),
+				Name:        jsonRule.Get("name").MustString(),
+				Scheduler:   jsonRule.Get("scheduler").MustInt64(),
+				Enabled:     jsonRule.Get("enabled").MustBool(),
+				Description: jsonRule.Get("description").MustString(),
+			}
+
+			valueQuery := jsonRule.Get("query")
+			valueQueryRef := valueQuery.Get("refId").MustString()
+			for _, targetsObj := range panel.Get("targets").MustArray() {
+				target := simplejson.NewFromAny(targetsObj)
+
+				if target.Get("refId").MustString() == valueQueryRef {
+					dsName := ""
+					if target.Get("datasource").MustString() != "" {
+						dsName = target.Get("datasource").MustString()
+					} else if panel.Get("datasource").MustString() != "" {
+						dsName = panel.Get("datasource").MustString()
+					}
+
+					if datasourceId, err := e.lookupDatasourceId(dsName); err != nil {
+						return nil, err
+					} else {
+						valueQuery.SetPath([]string{"datasourceId"}, datasourceId)
+					}
+
+					targetQuery := target.Get("target").MustString()
+					if targetQuery != "" {
+						jsonRule.SetPath([]string{"query", "query"}, targetQuery)
+					}
+				}
+			}
+
+			ruleModel.Expression = jsonRule
+
+			// validate
+			_, err := NewAlertRuleFromDBModel(ruleModel)
+			if err == nil && ruleModel.ValidToSave() {
+				rules = append(rules, ruleModel)
+			} else {
+				e.log.Error("Failed to extract alert rules from dashboard", "error", err)
+				return nil, errors.New("Failed to extract alert rules from dashboard")
+			}
+
+		}
+	}
+
+	return rules, nil
+}

+ 64 - 46
pkg/services/sqlstore/dashboard_parser_test.go → pkg/services/alerting/extractor_test.go

@@ -1,17 +1,17 @@
-package sqlstore
+package alerting
 
 import (
 	"testing"
 
+	"github.com/grafana/grafana/pkg/bus"
 	"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 TestAlertModelParsing(t *testing.T) {
+func TestAlertRuleExtraction(t *testing.T) {
 
-	Convey("Parsing alert info from json", t, func() {
+	Convey("Parsing alert rules  from dashboard json", t, func() {
 		Convey("Parsing and validating alerts from dashboards", func() {
 			json := `{
   "id": 57,
@@ -37,13 +37,15 @@ func TestAlertModelParsing(t *testing.T) {
           ],
           "datasource": null,
           "alerting": {
-            "name": "Alerting Panel Title alert",
-            "description": "description",
+            "name": "name1",
+            "description": "desc1",
+						"scheduler": 1,
+						"enabled": true,
             "critical": {
               "level": 20,
               "op": ">"
             },
-            "frequency": 10,
+            "frequency": "60s",
             "query": {
               "from": "5m",
               "refId": "A",
@@ -51,12 +53,12 @@ func TestAlertModelParsing(t *testing.T) {
             },
             "transform": {
               "method": "avg",
-              "name": "aggregation"
+              "type": "aggregation"
             },
             "warning": {
               "level": 10,
               "op": ">"
-            }           
+            }
           }
         },
         {
@@ -70,13 +72,15 @@ func TestAlertModelParsing(t *testing.T) {
           ],
           "datasource": "graphite2",
           "alerting": {
-            "name": "Alerting Panel Title alert",
-            "description": "description",
+            "name": "name2",
+            "description": "desc2",
+						"scheduler": 0,
+						"enabled": true,
             "critical": {
               "level": 20,
               "op": ">"
             },
-            "frequency": 10,
+            "frequency": "60s",
             "query": {
               "from": "5m",
               "refId": "A",
@@ -145,7 +149,10 @@ func TestAlertModelParsing(t *testing.T) {
           ],
           "title": "Broken influxdb panel",
           "transform": "table",
-          "type": "table"
+          "type": "table",
+					"alerting": {
+						"deleted": true
+					}
         }
       ],
       "title": "New row"
@@ -153,51 +160,62 @@ func TestAlertModelParsing(t *testing.T) {
   ]
 
 }`
-			dashboardJSON, _ := simplejson.NewJson([]byte(json))
-			cmd := &m.SaveDashboardCommand{
-				Dashboard: dashboardJSON,
-				UserId:    1,
-				OrgId:     1,
-				Overwrite: true,
-				Result: &m.Dashboard{
-					Id: 1,
-				},
-			}
-
-			InitTestDB(t)
-
-			AddDataSource(&m.AddDataSourceCommand{
-				Name:      "graphite2",
-				OrgId:     1,
-				Type:      m.DS_INFLUXDB,
-				Access:    m.DS_ACCESS_DIRECT,
-				Url:       "http://test",
-				IsDefault: false,
-				Database:  "site",
+			dashJson, err := simplejson.NewJson([]byte(json))
+			So(err, ShouldBeNil)
+
+			dash := m.NewDashboardFromJson(dashJson)
+			extractor := NewAlertRuleExtractor(dash, 1)
+
+			// mock data
+			defaultDs := &m.DataSource{Id: 12, OrgId: 2, Name: "I am default", IsDefault: true}
+			graphite2Ds := &m.DataSource{Id: 15, OrgId: 2, Name: "graphite2"}
+
+			bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
+				query.Result = []*m.DataSource{defaultDs, graphite2Ds}
+				return nil
 			})
 
-			AddDataSource(&m.AddDataSourceCommand{
-				Name:      "InfluxDB",
-				OrgId:     1,
-				Type:      m.DS_GRAPHITE,
-				Access:    m.DS_ACCESS_DIRECT,
-				Url:       "http://test",
-				IsDefault: true,
+			bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error {
+				if query.Name == defaultDs.Name {
+					query.Result = defaultDs
+				}
+				if query.Name == graphite2Ds.Name {
+					query.Result = graphite2Ds
+				}
+				return nil
 			})
 
-			alerts := alerting.ParseAlertsFromDashboard(cmd)
+			alerts, err := extractor.GetRuleModels()
+
+			Convey("Get rules without error", func() {
+				So(err, ShouldBeNil)
+			})
 
 			Convey("all properties have been set", func() {
-				So(alerts, ShouldNotBeEmpty)
 				So(len(alerts), ShouldEqual, 2)
 
 				for _, v := range alerts {
-					So(v.DashboardId, ShouldEqual, 1)
-					So(v.PanelId, ShouldNotEqual, 0)
-
+					So(v.DashboardId, ShouldEqual, 57)
 					So(v.Name, ShouldNotBeEmpty)
 					So(v.Description, ShouldNotBeEmpty)
 				}
+
+				Convey("should extract scheduler property", func() {
+					So(alerts[0].Scheduler, ShouldEqual, 1)
+					So(alerts[1].Scheduler, ShouldEqual, 0)
+				})
+
+				Convey("should extract panel idc", func() {
+					So(alerts[0].PanelId, ShouldEqual, 3)
+					So(alerts[1].PanelId, ShouldEqual, 4)
+				})
+
+				Convey("should extract name and desc", func() {
+					So(alerts[0].Name, ShouldEqual, "name1")
+					So(alerts[0].Description, ShouldEqual, "desc1")
+					So(alerts[1].Name, ShouldEqual, "name2")
+					So(alerts[1].Description, ShouldEqual, "desc2")
+				})
 			})
 		})
 	})

+ 0 - 21
pkg/services/alerting/models.go

@@ -1,9 +1,5 @@
 package alerting
 
-import (
-	"github.com/grafana/grafana/pkg/components/simplejson"
-)
-
 type AlertJob struct {
 	Offset     int64
 	Delay      bool
@@ -21,23 +17,6 @@ type AlertResult struct {
 	AlertJob    *AlertJob
 }
 
-type AlertRule struct {
-	Id              int64
-	OrgId           int64
-	DashboardId     int64
-	PanelId         int64
-	Frequency       int64
-	Name            string
-	Description     string
-	State           string
-	Warning         Level
-	Critical        Level
-	Query           AlertQuery
-	Transform       string
-	TransformParams simplejson.Json
-	Transformer     Transformer
-}
-
 type Level struct {
 	Operator string
 	Level    float64

+ 3 - 0
pkg/services/sqlstore/migrations/alert_mig.go

@@ -17,6 +17,9 @@ func addAlertMigrations(mg *Migrator) {
 			{Name: "description", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "expression", Type: DB_Text, Nullable: false},
+			{Name: "scheduler", Type: DB_BigInt, Nullable: false},
+			{Name: "frequency", Type: DB_BigInt, Nullable: false},
+			{Name: "enabled", Type: DB_Bool, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 		},

+ 22 - 12
public/app/plugins/panel/graph/alert_tab_ctrl.ts

@@ -24,6 +24,7 @@ export class AlertTabCtrl {
   panelCtrl: any;
   alerting: any;
   metricTargets = [{ refId: '- select query -' } ];
+  schedulers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}];
   transforms = [
     {
       text: 'Aggregation',
@@ -33,24 +34,23 @@ export class AlertTabCtrl {
       text: 'Linear Forecast',
       type: 'forecast',
     },
-    {
-      text: 'Percent Change',
-      type: 'percent_change',
-    },
-    {
-      text: 'Query diff',
-      type: 'query_diff',
-    },
   ];
   aggregators = ['avg', 'sum', 'min', 'max', 'last'];
   rule: any;
   query: any;
   queryParams: any;
   transformDef: any;
-  trasnformQuery: any;
+  levelOpList = [
+    {text: '>', value: '>'},
+    {text: '<', value: '<'},
+    {text: '=', value: '='},
+  ];
 
   defaultValues = {
-    frequency: 10,
+    frequency: '60s',
+    notify: [],
+    enabled: false,
+    scheduler: 1,
     warning: { op: '>', level: undefined },
     critical: { op: '>', level: undefined },
     query: {
@@ -139,8 +139,18 @@ export class AlertTabCtrl {
     }
   }
 
-  markAsDeleted() {
-    this.panel.alerting = this.defaultValues;
+  delete() {
+    this.rule = this.panel.alerting = this.defaultValues;
+    this.rule.deleted = true;
+  }
+
+  enable() {
+    delete this.rule.deleted;
+    this.rule.enabled = true;
+  }
+
+  disable() {
+    this.rule.enabled = false;
   }
 
   thresholdsUpdated() {

+ 54 - 47
public/app/plugins/panel/graph/partials/tab_alerting.html

@@ -1,15 +1,13 @@
-
-  <div class="gf-form-group" >
-    <h5 class="section-heading">Alert Rule</h5>
+<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>
@@ -33,77 +31,86 @@
       </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-7" type="text" ng-model="ctrl.rule.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
+        <input class="gf-form-input max-width-5" type="text" ng-model="ctrl.rule.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
       </div>
     </div>
   </div>
 
-  <div class="gf-form-group" >
+  <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 value
+          Warn if
         </span>
-        <span class="gf-form-label">
-          &gt;
-        </span>
-        <input class="gf-form-input max-width-7" type="number" ng-model="ctrl.rule.warnLevel" ng-change="alertTab.thresholdsUpdated()"></input>
+        <metric-segment-model property="ctrl.rule.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.rule.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 value
-        </span>
-        <span class="gf-form-label">
-          &gt;
+          Critcal if
         </span>
-        <input class="gf-form-input max-width-7" type="number" ng-model="ctrl.rule.critLevel" ng-change="alertTab.thresholdsUpdated()"></input>
+        <metric-segment-model property="ctrl.rule.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.rule.critLevel" ng-change="ctrl.thresholdsUpdated()"></input>
       </div>
     </div>
   </div>
+</div>
 
-  <!--   <div class="gf&#45;form"> -->
-  <!--     <span class="gf&#45;form&#45;label width&#45;12">Aggregation method</span> -->
-  <!--     <div class="gf&#45;form&#45;select&#45;wrapper max&#45;width&#45;10"> -->
-  <!--       <select class="gf&#45;form&#45;input" -->
-  <!--         ng&#45;model="ctrl.panel.alerting.aggregator" -->
-  <!--         ng&#45;options="oper as oper for oper in alertTab.aggregators"></select> -->
-  <!--     </div> -->
-  <!--   </div> -->
-  <!--  -->
-  <!--   <div class="gf&#45;form"> -->
-  <!--     <span class="gf&#45;form&#45;label width&#45;12">Query range  (seconds)</span> -->
-  <!--     <input class="gf&#45;form&#45;input max&#45;width&#45;10" type="number" -->
-  <!--       ng&#45;model="ctrl.panel.alerting.queryRange" placeholder="3600"></input> -->
-  <!--   </div> -->
-  <!--  -->
-  <!--   <div class="gf&#45;form"> -->
-  <!--     <span class="gf&#45;form&#45;label width&#45;12">Frequency (seconds)</span> -->
-  <!--     <input class="gf&#45;form&#45;input max&#45;width&#45;10" type="number" -->
-  <!--       ng&#45;model="ctrl.panel.alerting.frequency" placeholder="60"></input> -->
-  <!--   </div> -->
-  <!-- </div> -->
-<div>
+<div class="editor-row">
   <div class="gf-form-group section">
-    <h5 class="section-heading">Alert info</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>
+    <h5 class="section-heading">Execution</h5>
     <div class="gf-form-inline">
       <div class="gf-form">
-        <span class="gf-form-label width-10" style="margin-top: -73px;">Alert description</span>
+        <span class="gf-form-label">Scheduler</span>
+        <div class="gf-form-select-wrapper">
+          <select   class="gf-form-input"
+                    ng-model="ctrl.rule.scheduler"
+                    ng-options="f.value as f.text for f in ctrl.schedulers">
+          </select>
+        </div>
       </div>
       <div class="gf-form">
-        <textarea rows="5" ng-model="ctrl.panel.alerting.description" class="gf-form-input width-22"></textarea>
+        <span class="gf-form-label">Evaluate every</span>
+        <input class="gf-form-input max-width-7" type="text" ng-model="ctrl.rule.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.rule.notify" tagclass="label label-tag" placeholder="add tags">
+				</bootstrap-tagsinput>
+      </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">
+      <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 class="editor-row">
   <div class="gf-form-button-row">
-    <button class="btn btn-warning" ng-click="alertTab.markAsDeleted()">Delete Alert</button>
+    <button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.rule.enabled">Delete</button>
+    <button class="btn btn-success" ng-click="ctrl.enable()" ng-hide="ctrl.rule.enabled">Enable</button>
+    <button class="btn btn-secondary" ng-click="ctrl.disable()" ng-show="ctrl.rule.enabled">Disable</button>
   </div>
 </div>

+ 1 - 0
public/sass/components/_tagsinput.scss

@@ -7,6 +7,7 @@
   background-color: $input-bg;
 
   input {
+    display: inline-block;
     border: none;
     border-right: 1px solid $tight-form-border;
     margin: 0px;

+ 1 - 1
public/vendor/tagsinput/bootstrap-tagsinput.js

@@ -35,7 +35,7 @@
     this.inputSize = Math.max(1, this.placeholderText.length);
 
     this.$container = $('<div class="bootstrap-tagsinput"></div>');
-    this.$input = $('<input class="tight-form-input" size="' +
+    this.$input = $('<input class="gf-form-input" size="' +
                     this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
 
     this.$element.after(this.$container);