Browse Source

Merge remote-tracking branch 'origin/master' into develop

Torkel Ödegaard 8 years ago
parent
commit
a7645b710d
45 changed files with 200 additions and 194 deletions
  1. 2 0
      CHANGELOG.md
  2. 3 0
      conf/defaults.ini
  3. 3 0
      conf/sample.ini
  4. 1 1
      docker/blocks/graphite/docker-compose.yaml
  5. 1 1
      docs/sources/http_api/auth.md
  6. 11 7
      docs/sources/installation/configuration.md
  7. 1 5
      pkg/api/dashboard.go
  8. 6 15
      pkg/models/org_user.go
  9. 0 8
      pkg/services/alerting/eval_context.go
  10. 0 16
      pkg/services/alerting/eval_context_test.go
  11. 5 0
      pkg/services/alerting/eval_handler.go
  12. 37 0
      pkg/services/alerting/eval_handler_test.go
  13. 1 1
      pkg/services/alerting/interfaces.go
  14. 5 19
      pkg/services/alerting/notifier.go
  15. 0 89
      pkg/services/alerting/notifier_test.go
  16. 8 1
      pkg/services/alerting/notifiers/base.go
  17. 32 0
      pkg/services/alerting/notifiers/base_test.go
  18. 4 0
      pkg/services/alerting/notifiers/dingding.go
  19. 1 1
      pkg/services/alerting/notifiers/dingding_test.go
  20. 4 0
      pkg/services/alerting/notifiers/email.go
  21. 4 0
      pkg/services/alerting/notifiers/hipchat.go
  22. 4 0
      pkg/services/alerting/notifiers/kafka.go
  23. 4 0
      pkg/services/alerting/notifiers/line.go
  24. 4 0
      pkg/services/alerting/notifiers/opsgenie.go
  25. 4 0
      pkg/services/alerting/notifiers/pagerduty.go
  26. 4 0
      pkg/services/alerting/notifiers/pushover.go
  27. 4 0
      pkg/services/alerting/notifiers/sensu.go
  28. 4 0
      pkg/services/alerting/notifiers/slack.go
  29. 0 1
      pkg/services/alerting/notifiers/slack_test.go
  30. 4 0
      pkg/services/alerting/notifiers/teams.go
  31. 4 0
      pkg/services/alerting/notifiers/telegram.go
  32. 4 0
      pkg/services/alerting/notifiers/threema.go
  33. 4 0
      pkg/services/alerting/notifiers/victorops.go
  34. 4 0
      pkg/services/alerting/notifiers/webhook.go
  35. 3 3
      pkg/services/alerting/notifiers/webhook_test.go
  36. 2 4
      pkg/services/alerting/result_handler.go
  37. 0 4
      pkg/services/guardian/guardian.go
  38. 6 0
      pkg/services/sqlstore/migrations/org_mig.go
  39. 3 1
      pkg/setting/setting.go
  40. 1 1
      public/app/features/admin/partials/edit_org.html
  41. 5 5
      public/app/features/admin/partials/edit_user.html
  42. 1 1
      public/app/features/org/partials/invite.html
  43. 1 1
      public/app/features/org/partials/orgUsers.html
  44. 0 8
      public/app/plugins/datasource/graphite/graphite_query.ts
  45. 1 1
      public/app/plugins/datasource/graphite/query_ctrl.ts

+ 2 - 0
CHANGELOG.md

@@ -51,6 +51,8 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when
 ## Fixes
 ## Fixes
 * **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952)
 * **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952)
 * **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951)
 * **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951)
+* **Alerting**: Fixes bug where rules evaluated as firing when all conditions was false and using OR operator. [#9318](https://github.com/grafana/grafana/issues/9318)
+* **Cloudwatch**: CloudWatch no longer display metrics' default alias [#10151](https://github.com/grafana/grafana/issues/10151), thx [@mtanda](https://github.com/mtanda)
 
 
 # 4.6.2 (2017-11-16)
 # 4.6.2 (2017-11-16)
 
 

+ 3 - 0
conf/defaults.ini

@@ -221,6 +221,9 @@ external_manage_link_url =
 external_manage_link_name =
 external_manage_link_name =
 external_manage_info =
 external_manage_info =
 
 
+# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
+viewers_can_edit = false
+
 [auth]
 [auth]
 # Set to true to disable (hide) the login form, useful if you use OAuth
 # Set to true to disable (hide) the login form, useful if you use OAuth
 disable_login_form = false
 disable_login_form = false

+ 3 - 0
conf/sample.ini

@@ -205,6 +205,9 @@ log_queries =
 ;external_manage_link_name =
 ;external_manage_link_name =
 ;external_manage_info =
 ;external_manage_info =
 
 
+# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
+;viewers_can_edit = false
+
 [auth]
 [auth]
 # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
 # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
 ;disable_login_form = false
 ;disable_login_form = false

+ 1 - 1
docker/blocks/graphite/docker-compose.yaml

@@ -1,4 +1,4 @@
-  graphite:
+  graphite09:
     build: blocks/graphite
     build: blocks/graphite
     ports:
     ports:
       - "8080:80"
       - "8080:80"

+ 1 - 1
docs/sources/http_api/auth.md

@@ -100,7 +100,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 JSON Body schema:
 JSON Body schema:
 
 
 - **name** – The key name
 - **name** – The key name
-- **role** – Sets the access level/Grafana Role for the key. Can be one of the following values: `Viewer`, `Editor`, `Read Only Editor` or `Admin`.
+- **role** – Sets the access level/Grafana Role for the key. Can be one of the following values: `Viewer`, `Editor` or `Admin`.
 
 
 **Example Response**:
 **Example Response**:
 
 

+ 11 - 7
docs/sources/installation/configuration.md

@@ -205,7 +205,7 @@ The database user (not applicable for `sqlite3`).
 
 
 ### password
 ### password
 
 
-The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with trippel quotes. Ex `"""#password;"""`
+The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with triple quotes. Ex `"""#password;"""`
 
 
 ### ssl_mode
 ### ssl_mode
 
 
@@ -214,19 +214,19 @@ For MySQL, use either `true`, `false`, or `skip-verify`.
 
 
 ### ca_cert_path
 ### ca_cert_path
 
 
-(MySQL only) The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`.
+The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`.
 
 
 ### client_key_path
 ### client_key_path
 
 
-(MySQL only) The path to the client key. Only if server requires client authentication.
+The path to the client key. Only if server requires client authentication.
 
 
 ### client_cert_path
 ### client_cert_path
 
 
-(MySQL only) The path to the client cert. Only if server requires client authentication.
+The path to the client cert. Only if server requires client authentication.
 
 
 ### server_cert_name
 ### server_cert_name
 
 
-(MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`.
+The common name field of the certificate used by the `mysql` or `postgres` server. Not necessary if `ssl_mode` is set to `skip-verify`.
 
 
 ### max_idle_conn
 ### max_idle_conn
 The maximum number of connections in the idle connection pool.
 The maximum number of connections in the idle connection pool.
@@ -292,10 +292,14 @@ organization to be created for that new user.
 
 
 The role new users will be assigned for the main organization (if the
 The role new users will be assigned for the main organization (if the
 above setting is set to true).  Defaults to `Viewer`, other valid
 above setting is set to true).  Defaults to `Viewer`, other valid
-options are `Admin` and `Editor` and `Read Only Editor`. e.g. :
+options are `Admin` and `Editor`. e.g. :
 
 
-`auto_assign_org_role = Read Only Editor`
+`auto_assign_org_role = Viewer`
 
 
+### viewers can edit
+
+Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
+Defaults to `false`.
 
 
 <hr>
 <hr>
 
 

+ 1 - 5
pkg/api/dashboard.go

@@ -229,10 +229,6 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
 	return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id})
 	return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id})
 }
 }
 
 
-func canEditDashboard(role m.RoleType) bool {
-	return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
-}
-
 func GetHomeDashboard(c *middleware.Context) Response {
 func GetHomeDashboard(c *middleware.Context) Response {
 	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
 	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
 	if err := bus.Dispatch(&prefsQuery); err != nil {
 	if err := bus.Dispatch(&prefsQuery); err != nil {
@@ -258,7 +254,7 @@ func GetHomeDashboard(c *middleware.Context) Response {
 
 
 	dash := dtos.DashboardFullWithMeta{}
 	dash := dtos.DashboardFullWithMeta{}
 	dash.Meta.IsHome = true
 	dash.Meta.IsHome = true
-	dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_READ_ONLY_EDITOR)
+	dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR)
 	dash.Meta.FolderTitle = "Root"
 	dash.Meta.FolderTitle = "Root"
 
 
 	jsonParser := json.NewDecoder(file)
 	jsonParser := json.NewDecoder(file)

+ 6 - 15
pkg/models/org_user.go

@@ -18,14 +18,13 @@ var (
 type RoleType string
 type RoleType string
 
 
 const (
 const (
-	ROLE_VIEWER           RoleType = "Viewer"
-	ROLE_EDITOR           RoleType = "Editor"
-	ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor"
-	ROLE_ADMIN            RoleType = "Admin"
+	ROLE_VIEWER RoleType = "Viewer"
+	ROLE_EDITOR RoleType = "Editor"
+	ROLE_ADMIN  RoleType = "Admin"
 )
 )
 
 
 func (r RoleType) IsValid() bool {
 func (r RoleType) IsValid() bool {
-	return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
+	return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR
 }
 }
 
 
 func (r RoleType) Includes(other RoleType) bool {
 func (r RoleType) Includes(other RoleType) bool {
@@ -33,16 +32,8 @@ func (r RoleType) Includes(other RoleType) bool {
 		return true
 		return true
 	}
 	}
 
 
-	if other == ROLE_READ_ONLY_EDITOR {
-		return r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
-	}
-
-	if other == ROLE_EDITOR {
-		return r == ROLE_EDITOR
-	}
-
-	if other == ROLE_VIEWER {
-		return r == ROLE_READ_ONLY_EDITOR || r == ROLE_EDITOR || r == ROLE_VIEWER
+	if r == ROLE_EDITOR {
+		return other != ROLE_ADMIN
 	}
 	}
 
 
 	return false
 	return false

+ 0 - 8
pkg/services/alerting/eval_context.go

@@ -75,14 +75,6 @@ func (c *EvalContext) ShouldUpdateAlertState() bool {
 	return c.Rule.State != c.PrevAlertState
 	return c.Rule.State != c.PrevAlertState
 }
 }
 
 
-func (c *EvalContext) ShouldSendNotification() bool {
-	if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) {
-		return false
-	}
-
-	return true
-}
-
 func (a *EvalContext) GetDurationMs() float64 {
 func (a *EvalContext) GetDurationMs() float64 {
 	return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
 	return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
 }
 }

+ 0 - 16
pkg/services/alerting/eval_context_test.go

@@ -28,21 +28,5 @@ func TestAlertingEvalContext(t *testing.T) {
 				So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
 				So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
 			})
 			})
 		})
 		})
-
-		Convey("Should send notifications", func() {
-			Convey("pending -> ok", func() {
-				ctx.PrevAlertState = models.AlertStatePending
-				ctx.Rule.State = models.AlertStateOK
-
-				So(ctx.ShouldSendNotification(), ShouldBeFalse)
-			})
-
-			Convey("ok -> alerting", func() {
-				ctx.PrevAlertState = models.AlertStateOK
-				ctx.Rule.State = models.AlertStateAlerting
-
-				So(ctx.ShouldSendNotification(), ShouldBeTrue)
-			})
-		})
 	})
 	})
 }
 }

+ 5 - 0
pkg/services/alerting/eval_handler.go

@@ -39,6 +39,11 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 			break
 			break
 		}
 		}
 
 
+		if i == 0 {
+			firing = cr.Firing
+			noDataFound = cr.NoDataFound
+		}
+
 		// calculating Firing based on operator
 		// calculating Firing based on operator
 		if cr.Operator == "or" {
 		if cr.Operator == "or" {
 			firing = firing || cr.Firing
 			firing = firing || cr.Firing

+ 37 - 0
pkg/services/alerting/eval_handler_test.go

@@ -36,6 +36,16 @@ func TestAlertingEvaluationHandler(t *testing.T) {
 			So(context.ConditionEvals, ShouldEqual, "true = true")
 			So(context.ConditionEvals, ShouldEqual, "true = true")
 		})
 		})
 
 
+		Convey("Show return triggered with single passing condition2", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
+			})
+
+			handler.Eval(context)
+			So(context.Firing, ShouldEqual, true)
+			So(context.ConditionEvals, ShouldEqual, "true = true")
+		})
+
 		Convey("Show return false with not passing asdf", func() {
 		Convey("Show return false with not passing asdf", func() {
 			context := NewEvalContext(context.TODO(), &Rule{
 			context := NewEvalContext(context.TODO(), &Rule{
 				Conditions: []Condition{
 				Conditions: []Condition{
@@ -131,6 +141,33 @@ func TestAlertingEvaluationHandler(t *testing.T) {
 			So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
 			So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
 		})
 		})
 
 
+		Convey("Should return false if no condition is firing using OR operator", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{
+					&conditionStub{firing: false, operator: "or"},
+					&conditionStub{firing: false, operator: "or"},
+					&conditionStub{firing: false, operator: "or"},
+				},
+			})
+
+			handler.Eval(context)
+			So(context.Firing, ShouldEqual, false)
+			So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false")
+		})
+
+		Convey("Should retuasdfrn no data if one condition has nodata", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{
+					&conditionStub{operator: "or", noData: false},
+					&conditionStub{operator: "or", noData: false},
+					&conditionStub{operator: "or", noData: false},
+				},
+			})
+
+			handler.Eval(context)
+			So(context.NoDataFound, ShouldBeFalse)
+		})
+
 		Convey("Should return no data if one condition has nodata", func() {
 		Convey("Should return no data if one condition has nodata", func() {
 			context := NewEvalContext(context.TODO(), &Rule{
 			context := NewEvalContext(context.TODO(), &Rule{
 				Conditions: []Condition{
 				Conditions: []Condition{

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

@@ -15,7 +15,7 @@ type Notifier interface {
 	Notify(evalContext *EvalContext) error
 	Notify(evalContext *EvalContext) error
 	GetType() string
 	GetType() string
 	NeedsImage() bool
 	NeedsImage() bool
-	PassesFilter(rule *Rule) bool
+	ShouldNotify(evalContext *EvalContext) bool
 
 
 	GetNotifierId() int64
 	GetNotifierId() int64
 	GetIsDefault() bool
 	GetIsDefault() bool

+ 5 - 19
pkg/services/alerting/notifier.go

@@ -24,7 +24,7 @@ type NotifierPlugin struct {
 }
 }
 
 
 type NotificationService interface {
 type NotificationService interface {
-	Send(context *EvalContext) error
+	SendIfNeeded(context *EvalContext) error
 }
 }
 
 
 func NewNotificationService() NotificationService {
 func NewNotificationService() NotificationService {
@@ -41,14 +41,12 @@ func newNotificationService() *notificationService {
 	}
 	}
 }
 }
 
 
-func (n *notificationService) Send(context *EvalContext) error {
-	notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
+func (n *notificationService) SendIfNeeded(context *EvalContext) error {
+	notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
-
 	if len(notifiers) == 0 {
 	if len(notifiers) == 0 {
 		return nil
 		return nil
 	}
 	}
@@ -110,7 +108,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
 	return nil
 	return nil
 }
 }
 
 
-func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
+func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
 	query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
 	query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
 
 
 	if err := bus.Dispatch(query); err != nil {
 	if err := bus.Dispatch(query); err != nil {
@@ -122,7 +120,7 @@ func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64,
 		if not, err := n.createNotifierFor(notification); err != nil {
 		if not, err := n.createNotifierFor(notification); err != nil {
 			return nil, err
 			return nil, err
 		} else {
 		} else {
-			if shouldUseNotification(not, context) {
+			if not.ShouldNotify(context) {
 				result = append(result, not)
 				result = append(result, not)
 			}
 			}
 		}
 		}
@@ -140,18 +138,6 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
 	return notifierPlugin.Factory(model)
 	return notifierPlugin.Factory(model)
 }
 }
 
 
-func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
-	if !context.Firing {
-		return true
-	}
-
-	if context.Error != nil {
-		return true
-	}
-
-	return notifier.PassesFilter(context.Rule)
-}
-
 type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
 type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
 
 
 var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
 var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)

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

@@ -1,89 +0,0 @@
-package alerting
-
-import (
-	"testing"
-
-	"fmt"
-
-	"github.com/grafana/grafana/pkg/models"
-	m "github.com/grafana/grafana/pkg/models"
-	. "github.com/smartystreets/goconvey/convey"
-)
-
-type FakeNotifier struct {
-	FakeMatchResult bool
-}
-
-func (fn *FakeNotifier) GetType() string {
-	return "FakeNotifier"
-}
-
-func (fn *FakeNotifier) NeedsImage() bool {
-	return true
-}
-
-func (n *FakeNotifier) GetNotifierId() int64 {
-	return 0
-}
-
-func (n *FakeNotifier) GetIsDefault() bool {
-	return false
-}
-
-func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
-
-func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
-	return fn.FakeMatchResult
-}
-
-func TestAlertNotificationExtraction(t *testing.T) {
-
-	Convey("Notifier tests", t, func() {
-		Convey("none firing alerts", func() {
-			ctx := &EvalContext{
-				Firing: false,
-				Rule: &Rule{
-					State: m.AlertStateAlerting,
-				},
-			}
-			notifier := &FakeNotifier{FakeMatchResult: false}
-
-			So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
-		})
-
-		Convey("execution error cannot be ignored", func() {
-			ctx := &EvalContext{
-				Firing: true,
-				Error:  fmt.Errorf("I used to be a programmer just like you"),
-				Rule: &Rule{
-					State: m.AlertStateOK,
-				},
-			}
-			notifier := &FakeNotifier{FakeMatchResult: false}
-
-			So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
-		})
-
-		Convey("firing alert that match", func() {
-			ctx := &EvalContext{
-				Firing: true,
-				Rule: &Rule{
-					State: models.AlertStateAlerting,
-				},
-			}
-			notifier := &FakeNotifier{FakeMatchResult: true}
-
-			So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
-		})
-
-		Convey("firing alert that dont match", func() {
-			ctx := &EvalContext{
-				Firing: true,
-				Rule:   &Rule{State: m.AlertStateOK},
-			}
-			notifier := &FakeNotifier{FakeMatchResult: false}
-
-			So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
-		})
-	})
-}

+ 8 - 1
pkg/services/alerting/notifiers/base.go

@@ -2,6 +2,7 @@ package notifiers
 
 
 import (
 import (
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
+	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/alerting"
 )
 )
 
 
@@ -25,7 +26,13 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model
 	}
 	}
 }
 }
 
 
-func (n *NotifierBase) PassesFilter(rule *alerting.Rule) bool {
+func defaultShouldNotify(context *alerting.EvalContext) bool {
+	if context.PrevAlertState == context.Rule.State {
+		return false
+	}
+	if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) {
+		return false
+	}
 	return true
 	return true
 }
 }
 
 

+ 32 - 0
pkg/services/alerting/notifiers/base_test.go

@@ -0,0 +1,32 @@
+package notifiers
+
+import (
+	"context"
+	"testing"
+
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestBaseNotifier(t *testing.T) {
+	Convey("Base notifier tests", t, func() {
+		Convey("should notify", func() {
+			Convey("pending -> ok", func() {
+				context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
+					State: m.AlertStatePending,
+				})
+				context.Rule.State = m.AlertStateOK
+				So(defaultShouldNotify(context), ShouldBeFalse)
+			})
+
+			Convey("ok -> alerting", func() {
+				context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
+					State: m.AlertStateOK,
+				})
+				context.Rule.State = m.AlertStateAlerting
+				So(defaultShouldNotify(context), ShouldBeTrue)
+			})
+		})
+	})
+}

+ 4 - 0
pkg/services/alerting/notifiers/dingding.go

@@ -38,6 +38,10 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
 	}, nil
 	}, nil
 }
 }
 
 
+func (this *DingDingNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 type DingDingNotifier struct {
 type DingDingNotifier struct {
 	NotifierBase
 	NotifierBase
 	Url string
 	Url string

+ 1 - 1
pkg/services/alerting/notifiers/dingding_test.go

@@ -9,7 +9,7 @@ import (
 )
 )
 
 
 func TestDingDingNotifier(t *testing.T) {
 func TestDingDingNotifier(t *testing.T) {
-	Convey("Line notifier tests", t, func() {
+	Convey("Dingding notifier tests", t, func() {
 		Convey("empty settings should return error", func() {
 		Convey("empty settings should return error", func() {
 			json := `{ }`
 			json := `{ }`
 
 

+ 4 - 0
pkg/services/alerting/notifiers/email.go

@@ -58,6 +58,10 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 	}, nil
 	}, nil
 }
 }
 
 
+func (this *EmailNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Sending alert notification to", "addresses", this.Addresses)
 	this.log.Info("Sending alert notification to", "addresses", this.Addresses)
 
 

+ 4 - 0
pkg/services/alerting/notifiers/hipchat.go

@@ -75,6 +75,10 @@ type HipChatNotifier struct {
 	log    log.Logger
 	log    log.Logger
 }
 }
 
 
+func (this *HipChatNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 	this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 
 

+ 4 - 0
pkg/services/alerting/notifiers/kafka.go

@@ -57,6 +57,10 @@ type KafkaNotifier struct {
 	log      log.Logger
 	log      log.Logger
 }
 }
 
 
+func (this *KafkaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
 
 
 	state := evalContext.Rule.State
 	state := evalContext.Rule.State

+ 4 - 0
pkg/services/alerting/notifiers/line.go

@@ -51,6 +51,10 @@ type LineNotifier struct {
 	log   log.Logger
 	log   log.Logger
 }
 }
 
 
+func (this *LineNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 	this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 
 

+ 4 - 0
pkg/services/alerting/notifiers/opsgenie.go

@@ -62,6 +62,10 @@ type OpsGenieNotifier struct {
 	log       log.Logger
 	log       log.Logger
 }
 }
 
 
+func (this *OpsGenieNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
 
 
 	var err error
 	var err error

+ 4 - 0
pkg/services/alerting/notifiers/pagerduty.go

@@ -63,6 +63,10 @@ type PagerdutyNotifier struct {
 	log         log.Logger
 	log         log.Logger
 }
 }
 
 
+func (this *PagerdutyNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
 
 
 	if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve {
 	if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve {

+ 4 - 0
pkg/services/alerting/notifiers/pushover.go

@@ -123,6 +123,10 @@ type PushoverNotifier struct {
 	log      log.Logger
 	log      log.Logger
 }
 }
 
 
+func (this *PushoverNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
 	ruleUrl, err := evalContext.GetRuleUrl()
 	ruleUrl, err := evalContext.GetRuleUrl()
 	if err != nil {
 	if err != nil {

+ 4 - 0
pkg/services/alerting/notifiers/sensu.go

@@ -71,6 +71,10 @@ type SensuNotifier struct {
 	log      log.Logger
 	log      log.Logger
 }
 }
 
 
+func (this *SensuNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Sending sensu result")
 	this.log.Info("Sending sensu result")
 
 

+ 4 - 0
pkg/services/alerting/notifiers/slack.go

@@ -98,6 +98,10 @@ type SlackNotifier struct {
 	log       log.Logger
 	log       log.Logger
 }
 }
 
 
+func (this *SlackNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 	this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 
 

+ 0 - 1
pkg/services/alerting/notifiers/slack_test.go

@@ -78,7 +78,6 @@ func TestSlackNotifier(t *testing.T) {
 				So(slackNotifier.Mention, ShouldEqual, "@carl")
 				So(slackNotifier.Mention, ShouldEqual, "@carl")
 				So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
 				So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
 			})
 			})
-
 		})
 		})
 	})
 	})
 }
 }

+ 4 - 0
pkg/services/alerting/notifiers/teams.go

@@ -47,6 +47,10 @@ type TeamsNotifier struct {
 	log       log.Logger
 	log       log.Logger
 }
 }
 
 
+func (this *TeamsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 	this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 
 

+ 4 - 0
pkg/services/alerting/notifiers/telegram.go

@@ -76,6 +76,10 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
 	}, nil
 	}, nil
 }
 }
 
 
+func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Sending alert notification to", "bot_token", this.BotToken)
 	this.log.Info("Sending alert notification to", "bot_token", this.BotToken)
 	this.log.Info("Sending alert notification to", "chat_id", this.ChatID)
 	this.log.Info("Sending alert notification to", "chat_id", this.ChatID)

+ 4 - 0
pkg/services/alerting/notifiers/threema.go

@@ -114,6 +114,10 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 	}, nil
 	}, nil
 }
 }
 
 
+func (this *ThreemaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
 	notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
 	notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
 	notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)
 	notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)

+ 4 - 0
pkg/services/alerting/notifiers/victorops.go

@@ -68,6 +68,10 @@ type VictoropsNotifier struct {
 	log         log.Logger
 	log         log.Logger
 }
 }
 
 
+func (this *VictoropsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 // Notify sends notification to Victorops via POST to URL endpoint
 // Notify sends notification to Victorops via POST to URL endpoint
 func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 	this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

+ 4 - 0
pkg/services/alerting/notifiers/webhook.go

@@ -65,6 +65,10 @@ type WebhookNotifier struct {
 	log        log.Logger
 	log        log.Logger
 }
 }
 
 
+func (this *WebhookNotifier) ShouldNotify(context *alerting.EvalContext) bool {
+	return defaultShouldNotify(context)
+}
+
 func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Sending webhook")
 	this.log.Info("Sending webhook")
 
 

+ 3 - 3
pkg/services/alerting/notifiers/webhook_test.go

@@ -18,7 +18,7 @@ func TestWebhookNotifier(t *testing.T) {
 				settingsJSON, _ := simplejson.NewJson([]byte(json))
 				settingsJSON, _ := simplejson.NewJson([]byte(json))
 				model := &m.AlertNotification{
 				model := &m.AlertNotification{
 					Name:     "ops",
 					Name:     "ops",
-					Type:     "email",
+					Type:     "webhook",
 					Settings: settingsJSON,
 					Settings: settingsJSON,
 				}
 				}
 
 
@@ -35,7 +35,7 @@ func TestWebhookNotifier(t *testing.T) {
 				settingsJSON, _ := simplejson.NewJson([]byte(json))
 				settingsJSON, _ := simplejson.NewJson([]byte(json))
 				model := &m.AlertNotification{
 				model := &m.AlertNotification{
 					Name:     "ops",
 					Name:     "ops",
-					Type:     "email",
+					Type:     "webhook",
 					Settings: settingsJSON,
 					Settings: settingsJSON,
 				}
 				}
 
 
@@ -44,7 +44,7 @@ func TestWebhookNotifier(t *testing.T) {
 
 
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(webhookNotifier.Name, ShouldEqual, "ops")
 				So(webhookNotifier.Name, ShouldEqual, "ops")
-				So(webhookNotifier.Type, ShouldEqual, "email")
+				So(webhookNotifier.Type, ShouldEqual, "webhook")
 				So(webhookNotifier.Url, ShouldEqual, "http://google.com")
 				So(webhookNotifier.Url, ShouldEqual, "http://google.com")
 			})
 			})
 		})
 		})

+ 2 - 4
pkg/services/alerting/result_handler.go

@@ -85,11 +85,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
 		if err := annotationRepo.Save(&item); err != nil {
 		if err := annotationRepo.Save(&item); err != nil {
 			handler.log.Error("Failed to save annotation for new alert state", "error", err)
 			handler.log.Error("Failed to save annotation for new alert state", "error", err)
 		}
 		}
-
-		if evalContext.ShouldSendNotification() {
-			handler.notifier.Send(evalContext)
-		}
 	}
 	}
 
 
+	handler.notifier.SendIfNeeded(evalContext)
+
 	return nil
 	return nil
 }
 }

+ 0 - 4
pkg/services/guardian/guardian.go

@@ -51,10 +51,6 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, er
 	}
 	}
 
 
 	orgRole := g.user.OrgRole
 	orgRole := g.user.OrgRole
-	if orgRole == m.ROLE_READ_ONLY_EDITOR {
-		orgRole = m.ROLE_VIEWER
-	}
-
 	teamAclItems := []*m.DashboardAclInfoDTO{}
 	teamAclItems := []*m.DashboardAclInfoDTO{}
 
 
 	for _, p := range acl {
 	for _, p := range acl {

+ 6 - 0
pkg/services/sqlstore/migrations/org_mig.go

@@ -83,4 +83,10 @@ func addOrgMigrations(mg *Migrator) {
 	mg.AddMigration("Update org_user table charset", NewTableCharsetMigration("org_user", []*Column{
 	mg.AddMigration("Update org_user table charset", NewTableCharsetMigration("org_user", []*Column{
 		{Name: "role", Type: DB_NVarchar, Length: 20},
 		{Name: "role", Type: DB_NVarchar, Length: 20},
 	}))
 	}))
+
+	const migrateReadOnlyViewersToViewers = `UPDATE org_user SET role = 'Viewer' WHERE role = 'Read Only Editor'`
+	mg.AddMigration("Migrate all Read Only Viewers to Viewers", new(RawSqlMigration).
+		Sqlite(migrateReadOnlyViewersToViewers).
+		Postgres(migrateReadOnlyViewersToViewers).
+		Mysql(migrateReadOnlyViewersToViewers))
 }
 }

+ 3 - 1
pkg/setting/setting.go

@@ -106,6 +106,7 @@ var (
 	ExternalUserMngLinkUrl  string
 	ExternalUserMngLinkUrl  string
 	ExternalUserMngLinkName string
 	ExternalUserMngLinkName string
 	ExternalUserMngInfo     string
 	ExternalUserMngInfo     string
+	ViewersCanEdit          bool
 
 
 	// Http auth
 	// Http auth
 	AdminUser     string
 	AdminUser     string
@@ -540,13 +541,14 @@ func NewConfigContext(args *CommandLineArgs) error {
 	AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
 	AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
 	AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
 	AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
 	AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
 	AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
-	AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"})
+	AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
 	LoginHint = users.Key("login_hint").String()
 	LoginHint = users.Key("login_hint").String()
 	DefaultTheme = users.Key("default_theme").String()
 	DefaultTheme = users.Key("default_theme").String()
 	ExternalUserMngLinkUrl = users.Key("external_manage_link_url").String()
 	ExternalUserMngLinkUrl = users.Key("external_manage_link_url").String()
 	ExternalUserMngLinkName = users.Key("external_manage_link_name").String()
 	ExternalUserMngLinkName = users.Key("external_manage_link_name").String()
 	ExternalUserMngInfo = users.Key("external_manage_info").String()
 	ExternalUserMngInfo = users.Key("external_manage_info").String()
+	ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false)
 
 
 	// auth
 	// auth
 	auth := Cfg.Section("auth")
 	auth := Cfg.Section("auth")

+ 1 - 1
public/app/features/admin/partials/edit_org.html

@@ -29,7 +29,7 @@
 			<td>
 			<td>
         <div class="gf-form">
         <div class="gf-form">
           <span class="gf-form-select-wrapper">
           <span class="gf-form-select-wrapper">
-              <select type="text" ng-model="orgUser.role" class="gf-form-input max-width-8" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(orgUser)">
+              <select type="text" ng-model="orgUser.role" class="gf-form-input max-width-8" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgUser(orgUser)">
               </select>
               </select>
           </span>
           </span>
         </div>
         </div>

+ 5 - 5
public/app/features/admin/partials/edit_user.html

@@ -59,10 +59,10 @@
 				<input type="text" ng-model="newOrg.name" bs-typeahead="searchOrgs"	required class="gf-form-input max-width-20" placeholder="organization name">
 				<input type="text" ng-model="newOrg.name" bs-typeahead="searchOrgs"	required class="gf-form-input max-width-20" placeholder="organization name">
 			</div>
 			</div>
 			<div class="gf-form">
 			<div class="gf-form">
-        <span class="gf-form-label">Role</span>
-        <span class="gf-form-select-wrapper">
-            <select type="text" ng-model="newOrg.role" class="gf-form-input width-10" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']"></select>
-        </span>
+        	<span class="gf-form-label">Role</span>
+        	<span class="gf-form-select-wrapper">
+            	<select type="text" ng-model="newOrg.role" class="gf-form-input width-10" ng-options="f for f in ['Viewer', 'Editor', 'Admin']"></select>
+        	</span>
 			</div>
 			</div>
 			<div class="gf-form">
 			<div class="gf-form">
 				<button class="btn btn-success gf-form-btn" ng-click="addOrgUser()">Add</button>
 				<button class="btn btn-success gf-form-btn" ng-click="addOrgUser()">Add</button>
@@ -85,7 +85,7 @@
 			<td>
 			<td>
         <div class="gf-form">
         <div class="gf-form">
             <span class="gf-form-select-wrapper">
             <span class="gf-form-select-wrapper">
-                <select type="text" ng-model="org.role" class="gf-form-input max-width-12" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(org)">
+                <select type="text" ng-model="org.role" class="gf-form-input max-width-12" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgUser(org)">
                 </select>
                 </select>
             </span>
             </span>
         </div>
         </div>

+ 1 - 1
public/app/features/org/partials/invite.html

@@ -21,7 +21,7 @@
 			</div>
 			</div>
 			<div class="gf-form max-width-30">
 			<div class="gf-form max-width-30">
         <span class="gf-form-label width-10">Role</span>
         <span class="gf-form-label width-10">Role</span>
-				<select ng-model="ctrl.invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
+				<select ng-model="ctrl.invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Admin']">
 				</select>
 				</select>
 			</div>
 			</div>
 
 

+ 1 - 1
public/app/features/org/partials/orgUsers.html

@@ -52,7 +52,7 @@
         <td>{{user.lastSeenAtAge}}</td>
         <td>{{user.lastSeenAtAge}}</td>
         <td>
         <td>
           <div class="gf-form-select-wrapper width-12">
           <div class="gf-form-select-wrapper width-12">
-            <select type="text" ng-model="user.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)">
+            <select type="text" ng-model="user.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)">
             </select>
             </select>
           </div>
           </div>
         </td>
         </td>

+ 0 - 8
public/app/plugins/datasource/graphite/graphite_query.ts

@@ -120,14 +120,6 @@ export default class GraphiteQuery {
     this.segments.push({value: "select metric"});
     this.segments.push({value: "select metric"});
   }
   }
 
 
-  hasSelectMetric() {
-    if (this.segments.length > 0) {
-      return this.segments[this.segments.length - 1].value === 'select metric';
-    } else {
-      return false;
-    }
-  }
-
   addFunction(newFunc) {
   addFunction(newFunc) {
     this.functions.push(newFunc);
     this.functions.push(newFunc);
     this.moveAliasFuncLast();
     this.moveAliasFuncLast();

+ 1 - 1
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -218,7 +218,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     var oldTarget = this.queryModel.target.target;
     var oldTarget = this.queryModel.target.target;
     this.updateModelTarget();
     this.updateModelTarget();
 
 
-    if (this.queryModel.target !== oldTarget && !this.queryModel.hasSelectMetric()) {
+    if (this.queryModel.target !== oldTarget) {
       this.panelCtrl.refresh();
       this.panelCtrl.refresh();
     }
     }
   }
   }