Przeglądaj źródła

Merge branch 'master' into datasource-dashboards-to-react

Peter Holmberg 7 lat temu
rodzic
commit
02769b6d3c

+ 7 - 1
CHANGELOG.md

@@ -4,16 +4,22 @@
 
 
 * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
 * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
 * **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
 * **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
+* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
 
 
 ### Minor
 ### Minor
 
 
-* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
+* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
 * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
 
 
 ### Breaking changes
 ### Breaking changes
 
 
 * Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
 * Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
 
 
+# 5.3.2 (unreleased)
+
+* **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm)
+
 # 5.3.1 (2018-10-16)
 # 5.3.1 (2018-10-16)
 
 
 * **Render**: Fix PhantomJS render of graph panel when legend displayed as table to the right [#13616](https://github.com/grafana/grafana/issues/13616)
 * **Render**: Fix PhantomJS render of graph panel when legend displayed as table to the right [#13616](https://github.com/grafana/grafana/issues/13616)

+ 1 - 1
README.md

@@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
 
 
 ### Dependencies
 ### Dependencies
 
 
-- Go 1.11
+- Go (Latest Stable)
 - NodeJS LTS
 - NodeJS LTS
 
 
 ### Building the backend
 ### Building the backend

+ 89 - 0
UPGRADING_DEPENDENCIES.md

@@ -0,0 +1,89 @@
+# Guide to Upgrading Dependencies
+
+Upgrading Go or Node.js requires making changes in many different files. See below for a list and explanation for each.
+
+## Go
+
+- CircleCi
+- `grafana/build-container`
+- Appveyor
+- Dockerfile
+
+## Node.js
+
+- CircleCI
+- `grafana/build-container`
+- Appveyor
+- Dockerfile
+
+## Go Dependencies
+
+Updated using `dep`.
+
+- `Gopkg.toml`
+- `Gopkg.lock`
+
+## Node.js Dependencies
+
+Updated using `yarn`.
+
+- `package.json`
+
+## Where to make changes
+
+### CircleCI
+
+Our builds run on CircleCI through our build script.
+
+#### Files
+
+- `.circleci/config.yml`.
+
+#### Dependencies
+
+- nodejs
+- golang
+- grafana/build-container (our custom docker build container)
+
+### grafana/build-container
+
+The main build step (in CircleCI) is built using a custom build container that comes pre-baked with some of the neccesary dependencies.
+
+Link: [grafana-build-container](https://github.com/grafana/grafana-build-container)
+
+#### Dependencies
+
+- fpm
+- nodejs
+- golang
+- crosscompiling (several compilers)
+
+### Appveyor
+
+Master and release builds trigger test runs on Appveyors build environment so that tests will run on Windows.
+
+#### Files:
+
+- `appveyor.yml`
+
+#### Dependencies
+
+- nodejs
+- golang
+
+### Dockerfile
+
+There is a Docker build for Grafana in the root of the project that allows anyone to build Grafana just using Docker.
+
+#### Files
+
+- `Dockerfile`
+
+#### Dependencies
+
+- nodejs
+- golang
+
+### Local developer environments
+
+Please send out a notice in the grafana-dev slack channel when updating Go or Node.js to make it easier for everyone to update their local developer environments.

+ 4 - 3
docs/sources/features/datasources/cloudwatch.md

@@ -46,7 +46,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
 ## IAM Policies
 ## IAM Policies
 
 
 Grafana needs permissions granted via IAM to be able to read CloudWatch metrics
 Grafana needs permissions granted via IAM to be able to read CloudWatch metrics
-and EC2 tags/instances. You can attach these permissions to IAM roles and
+and EC2 tags/instances/regions. You can attach these permissions to IAM roles and
 utilize Grafana's built-in support for assuming roles.
 utilize Grafana's built-in support for assuming roles.
 
 
 Here is a minimal policy example:
 Here is a minimal policy example:
@@ -65,11 +65,12 @@ Here is a minimal policy example:
             "Resource": "*"
             "Resource": "*"
         },
         },
         {
         {
-            "Sid": "AllowReadingTagsFromEC2",
+            "Sid": "AllowReadingTagsInstancesRegionsFromEC2",
             "Effect": "Allow",
             "Effect": "Allow",
             "Action": [
             "Action": [
                 "ec2:DescribeTags",
                 "ec2:DescribeTags",
-                "ec2:DescribeInstances"
+                "ec2:DescribeInstances",
+                "ec2:DescribeRegions"
             ],
             ],
             "Resource": "*"
             "Resource": "*"
         }
         }

+ 16 - 1
docs/sources/installation/docker.md

@@ -87,7 +87,7 @@ docker run \
 
 
 ## Building a custom Grafana image with pre-installed plugins
 ## Building a custom Grafana image with pre-installed plugins
 
 
-In the [grafana-docker](https://github.com/grafana/grafana-docker/)  there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image.  It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments.
+In the [grafana-docker](https://github.com/grafana/grafana/tree/master/packaging/docker)  there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image.  It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments.
 
 
 Example of how to build and run:
 Example of how to build and run:
 ```bash
 ```bash
@@ -103,6 +103,21 @@ docker run \
   grafana:latest-with-plugins
   grafana:latest-with-plugins
 ```
 ```
 
 
+## Installing Plugins from other sources
+
+> Only available in Grafana v5.3.1+
+
+It's possible to install plugins from custom url:s by specifying the url like this: `GF_INSTALL_PLUGINS=<url to plugin zip>;<plugin name>`
+
+```bash
+docker run \
+  -d \
+  -p 3000:3000 \
+  --name=grafana \
+  -e "GF_INSTALL_PLUGINS=http://plugin-domain.com/my-custom-plugin.zip;custom-plugin" \
+  grafana/grafana
+```
+
 ## Configuring AWS Credentials for CloudWatch Support
 ## Configuring AWS Credentials for CloudWatch Support
 
 
 ```bash
 ```bash

+ 1 - 1
docs/sources/project/building_from_source.md

@@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
 
 
 ## Dependencies
 ## Dependencies
 
 
-- [Go 1.11](https://golang.org/dl/)
+- [Go (Latest Stable)](https://golang.org/dl/)
 - [Git](https://git-scm.com/downloads)
 - [Git](https://git-scm.com/downloads)
 - [NodeJS LTS](https://nodejs.org/download/)
 - [NodeJS LTS](https://nodejs.org/download/)
 - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.
 - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.

+ 1 - 17
pkg/api/datasources.go

@@ -17,24 +17,8 @@ func GetDataSources(c *m.ReqContext) Response {
 		return Error(500, "Failed to query datasources", err)
 		return Error(500, "Failed to query datasources", err)
 	}
 	}
 
 
-	dsFilterQuery := m.DatasourcesPermissionFilterQuery{
-		User:        c.SignedInUser,
-		Datasources: query.Result,
-	}
-
-	var datasources []*m.DataSource
-	if err := bus.Dispatch(&dsFilterQuery); err != nil {
-		if err != bus.ErrHandlerNotFound {
-			return Error(500, "Could not get datasources", err)
-		}
-
-		datasources = query.Result
-	} else {
-		datasources = dsFilterQuery.Result
-	}
-
 	result := make(dtos.DataSourceList, 0)
 	result := make(dtos.DataSourceList, 0)
-	for _, ds := range datasources {
+	for _, ds := range query.Result {
 		dsItem := dtos.DataSourceListItemDTO{
 		dsItem := dtos.DataSourceListItemDTO{
 			OrgId:     ds.OrgId,
 			OrgId:     ds.OrgId,
 			Id:        ds.Id,
 			Id:        ds.Id,

+ 26 - 23
pkg/api/dtos/alerting.go

@@ -49,28 +49,30 @@ func formatShort(interval time.Duration) string {
 
 
 func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
 func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
 	return &AlertNotification{
 	return &AlertNotification{
-		Id:           notification.Id,
-		Name:         notification.Name,
-		Type:         notification.Type,
-		IsDefault:    notification.IsDefault,
-		Created:      notification.Created,
-		Updated:      notification.Updated,
-		Frequency:    formatShort(notification.Frequency),
-		SendReminder: notification.SendReminder,
-		Settings:     notification.Settings,
+		Id:                    notification.Id,
+		Name:                  notification.Name,
+		Type:                  notification.Type,
+		IsDefault:             notification.IsDefault,
+		Created:               notification.Created,
+		Updated:               notification.Updated,
+		Frequency:             formatShort(notification.Frequency),
+		SendReminder:          notification.SendReminder,
+		DisableResolveMessage: notification.DisableResolveMessage,
+		Settings:              notification.Settings,
 	}
 	}
 }
 }
 
 
 type AlertNotification struct {
 type AlertNotification struct {
-	Id           int64            `json:"id"`
-	Name         string           `json:"name"`
-	Type         string           `json:"type"`
-	IsDefault    bool             `json:"isDefault"`
-	SendReminder bool             `json:"sendReminder"`
-	Frequency    string           `json:"frequency"`
-	Created      time.Time        `json:"created"`
-	Updated      time.Time        `json:"updated"`
-	Settings     *simplejson.Json `json:"settings"`
+	Id                    int64            `json:"id"`
+	Name                  string           `json:"name"`
+	Type                  string           `json:"type"`
+	IsDefault             bool             `json:"isDefault"`
+	SendReminder          bool             `json:"sendReminder"`
+	DisableResolveMessage bool             `json:"disableResolveMessage"`
+	Frequency             string           `json:"frequency"`
+	Created               time.Time        `json:"created"`
+	Updated               time.Time        `json:"updated"`
+	Settings              *simplejson.Json `json:"settings"`
 }
 }
 
 
 type AlertTestCommand struct {
 type AlertTestCommand struct {
@@ -100,11 +102,12 @@ type EvalMatch struct {
 }
 }
 
 
 type NotificationTestCommand struct {
 type NotificationTestCommand struct {
-	Name         string           `json:"name"`
-	Type         string           `json:"type"`
-	SendReminder bool             `json:"sendReminder"`
-	Frequency    string           `json:"frequency"`
-	Settings     *simplejson.Json `json:"settings"`
+	Name                  string           `json:"name"`
+	Type                  string           `json:"type"`
+	SendReminder          bool             `json:"sendReminder"`
+	DisableResolveMessage bool             `json:"disableResolveMessage"`
+	Frequency             string           `json:"frequency"`
+	Settings              *simplejson.Json `json:"settings"`
 }
 }
 
 
 type PauseAlertCommand struct {
 type PauseAlertCommand struct {

+ 26 - 23
pkg/models/alert_notifications.go

@@ -23,38 +23,41 @@ var (
 )
 )
 
 
 type AlertNotification struct {
 type AlertNotification struct {
-	Id           int64            `json:"id"`
-	OrgId        int64            `json:"-"`
-	Name         string           `json:"name"`
-	Type         string           `json:"type"`
-	SendReminder bool             `json:"sendReminder"`
-	Frequency    time.Duration    `json:"frequency"`
-	IsDefault    bool             `json:"isDefault"`
-	Settings     *simplejson.Json `json:"settings"`
-	Created      time.Time        `json:"created"`
-	Updated      time.Time        `json:"updated"`
+	Id                    int64            `json:"id"`
+	OrgId                 int64            `json:"-"`
+	Name                  string           `json:"name"`
+	Type                  string           `json:"type"`
+	SendReminder          bool             `json:"sendReminder"`
+	DisableResolveMessage bool             `json:"disableResolveMessage"`
+	Frequency             time.Duration    `json:"frequency"`
+	IsDefault             bool             `json:"isDefault"`
+	Settings              *simplejson.Json `json:"settings"`
+	Created               time.Time        `json:"created"`
+	Updated               time.Time        `json:"updated"`
 }
 }
 
 
 type CreateAlertNotificationCommand struct {
 type CreateAlertNotificationCommand struct {
-	Name         string           `json:"name"  binding:"Required"`
-	Type         string           `json:"type"  binding:"Required"`
-	SendReminder bool             `json:"sendReminder"`
-	Frequency    string           `json:"frequency"`
-	IsDefault    bool             `json:"isDefault"`
-	Settings     *simplejson.Json `json:"settings"`
+	Name                  string           `json:"name"  binding:"Required"`
+	Type                  string           `json:"type"  binding:"Required"`
+	SendReminder          bool             `json:"sendReminder"`
+	DisableResolveMessage bool             `json:"disableResolveMessage"`
+	Frequency             string           `json:"frequency"`
+	IsDefault             bool             `json:"isDefault"`
+	Settings              *simplejson.Json `json:"settings"`
 
 
 	OrgId  int64 `json:"-"`
 	OrgId  int64 `json:"-"`
 	Result *AlertNotification
 	Result *AlertNotification
 }
 }
 
 
 type UpdateAlertNotificationCommand struct {
 type UpdateAlertNotificationCommand struct {
-	Id           int64            `json:"id"  binding:"Required"`
-	Name         string           `json:"name"  binding:"Required"`
-	Type         string           `json:"type"  binding:"Required"`
-	SendReminder bool             `json:"sendReminder"`
-	Frequency    string           `json:"frequency"`
-	IsDefault    bool             `json:"isDefault"`
-	Settings     *simplejson.Json `json:"settings"  binding:"Required"`
+	Id                    int64            `json:"id"  binding:"Required"`
+	Name                  string           `json:"name"  binding:"Required"`
+	Type                  string           `json:"type"  binding:"Required"`
+	SendReminder          bool             `json:"sendReminder"`
+	DisableResolveMessage bool             `json:"disableResolveMessage"`
+	Frequency             string           `json:"frequency"`
+	IsDefault             bool             `json:"isDefault"`
+	Settings              *simplejson.Json `json:"settings"  binding:"Required"`
 
 
 	OrgId  int64 `json:"-"`
 	OrgId  int64 `json:"-"`
 	Result *AlertNotification
 	Result *AlertNotification

+ 2 - 8
pkg/models/datasource.go

@@ -195,8 +195,8 @@ type GetDataSourceByNameQuery struct {
 type DsPermissionType int
 type DsPermissionType int
 
 
 const (
 const (
-	DsPermissionQuery DsPermissionType = 1 << iota
-	DsPermissionNoAccess
+	DsPermissionNoAccess DsPermissionType = iota
+	DsPermissionQuery
 )
 )
 
 
 func (p DsPermissionType) String() string {
 func (p DsPermissionType) String() string {
@@ -207,12 +207,6 @@ func (p DsPermissionType) String() string {
 	return names[int(p)]
 	return names[int(p)]
 }
 }
 
 
-type HasRequiredDataSourcePermissionQuery struct {
-	Id                 int64
-	User               *SignedInUser
-	RequiredPermission DsPermissionType
-}
-
 type GetDataSourcePermissionsForUserQuery struct {
 type GetDataSourcePermissionsForUserQuery struct {
 	User   *SignedInUser
 	User   *SignedInUser
 	Result map[int64]DsPermissionType
 	Result map[int64]DsPermissionType

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

@@ -27,6 +27,7 @@ type Notifier interface {
 	GetNotifierId() int64
 	GetNotifierId() int64
 	GetIsDefault() bool
 	GetIsDefault() bool
 	GetSendReminder() bool
 	GetSendReminder() bool
+	GetDisableResolveMessage() bool
 	GetFrequency() time.Duration
 	GetFrequency() time.Duration
 }
 }
 
 

+ 26 - 16
pkg/services/alerting/notifiers/base.go

@@ -6,7 +6,6 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
-
 	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/alerting"
 )
 )
 
 
@@ -15,13 +14,14 @@ const (
 )
 )
 
 
 type NotifierBase struct {
 type NotifierBase struct {
-	Name         string
-	Type         string
-	Id           int64
-	IsDeault     bool
-	UploadImage  bool
-	SendReminder bool
-	Frequency    time.Duration
+	Name                  string
+	Type                  string
+	Id                    int64
+	IsDeault              bool
+	UploadImage           bool
+	SendReminder          bool
+	DisableResolveMessage bool
+	Frequency             time.Duration
 
 
 	log log.Logger
 	log log.Logger
 }
 }
@@ -34,14 +34,15 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
 	}
 	}
 
 
 	return NotifierBase{
 	return NotifierBase{
-		Id:           model.Id,
-		Name:         model.Name,
-		IsDeault:     model.IsDefault,
-		Type:         model.Type,
-		UploadImage:  uploadImage,
-		SendReminder: model.SendReminder,
-		Frequency:    model.Frequency,
-		log:          log.New("alerting.notifier." + model.Name),
+		Id:                    model.Id,
+		Name:                  model.Name,
+		IsDeault:              model.IsDefault,
+		Type:                  model.Type,
+		UploadImage:           uploadImage,
+		SendReminder:          model.SendReminder,
+		DisableResolveMessage: model.DisableResolveMessage,
+		Frequency:             model.Frequency,
+		log:                   log.New("alerting.notifier." + model.Name),
 	}
 	}
 }
 }
 
 
@@ -83,6 +84,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC
 		}
 		}
 	}
 	}
 
 
+	// Do not notify when state is OK if DisableResolveMessage is set to true
+	if context.Rule.State == models.AlertStateOK && n.DisableResolveMessage {
+		return false
+	}
+
 	return true
 	return true
 }
 }
 
 
@@ -106,6 +112,10 @@ func (n *NotifierBase) GetSendReminder() bool {
 	return n.SendReminder
 	return n.SendReminder
 }
 }
 
 
+func (n *NotifierBase) GetDisableResolveMessage() bool {
+	return n.DisableResolveMessage
+}
+
 func (n *NotifierBase) GetFrequency() time.Duration {
 func (n *NotifierBase) GetFrequency() time.Duration {
 	return n.Frequency
 	return n.Frequency
 }
 }

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

@@ -179,5 +179,10 @@ func TestBaseNotifier(t *testing.T) {
 			base := NewNotifierBase(model)
 			base := NewNotifierBase(model)
 			So(base.UploadImage, ShouldBeTrue)
 			So(base.UploadImage, ShouldBeTrue)
 		})
 		})
+
+		Convey("default value should be false for backwards compatibility", func() {
+			base := NewNotifierBase(model)
+			So(base.DisableResolveMessage, ShouldBeFalse)
+		})
 	})
 	})
 }
 }

+ 14 - 10
pkg/services/sqlstore/alert_notification.go

@@ -66,6 +66,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
 										alert_notification.updated,
 										alert_notification.updated,
 										alert_notification.settings,
 										alert_notification.settings,
 										alert_notification.is_default,
 										alert_notification.is_default,
+										alert_notification.disable_resolve_message,
 										alert_notification.send_reminder,
 										alert_notification.send_reminder,
 										alert_notification.frequency
 										alert_notification.frequency
 										FROM alert_notification
 										FROM alert_notification
@@ -106,6 +107,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
 										alert_notification.updated,
 										alert_notification.updated,
 										alert_notification.settings,
 										alert_notification.settings,
 										alert_notification.is_default,
 										alert_notification.is_default,
+										alert_notification.disable_resolve_message,
 										alert_notification.send_reminder,
 										alert_notification.send_reminder,
 										alert_notification.frequency
 										alert_notification.frequency
 										FROM alert_notification
 										FROM alert_notification
@@ -166,15 +168,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
 		}
 		}
 
 
 		alertNotification := &m.AlertNotification{
 		alertNotification := &m.AlertNotification{
-			OrgId:        cmd.OrgId,
-			Name:         cmd.Name,
-			Type:         cmd.Type,
-			Settings:     cmd.Settings,
-			SendReminder: cmd.SendReminder,
-			Frequency:    frequency,
-			Created:      time.Now(),
-			Updated:      time.Now(),
-			IsDefault:    cmd.IsDefault,
+			OrgId:                 cmd.OrgId,
+			Name:                  cmd.Name,
+			Type:                  cmd.Type,
+			Settings:              cmd.Settings,
+			SendReminder:          cmd.SendReminder,
+			DisableResolveMessage: cmd.DisableResolveMessage,
+			Frequency:             frequency,
+			Created:               time.Now(),
+			Updated:               time.Now(),
+			IsDefault:             cmd.IsDefault,
 		}
 		}
 
 
 		if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil {
 		if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil {
@@ -210,6 +213,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 		current.Type = cmd.Type
 		current.Type = cmd.Type
 		current.IsDefault = cmd.IsDefault
 		current.IsDefault = cmd.IsDefault
 		current.SendReminder = cmd.SendReminder
 		current.SendReminder = cmd.SendReminder
+		current.DisableResolveMessage = cmd.DisableResolveMessage
 
 
 		if current.SendReminder {
 		if current.SendReminder {
 			if cmd.Frequency == "" {
 			if cmd.Frequency == "" {
@@ -224,7 +228,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 			current.Frequency = frequency
 			current.Frequency = frequency
 		}
 		}
 
 
-		sess.UseBool("is_default", "send_reminder")
+		sess.UseBool("is_default", "send_reminder", "disable_resolve_message")
 
 
 		if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
 		if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
 			return err
 			return err

+ 10 - 7
pkg/services/sqlstore/alert_notification_test.go

@@ -219,6 +219,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
 			So(cmd.Result.OrgId, ShouldNotEqual, 0)
 			So(cmd.Result.OrgId, ShouldNotEqual, 0)
 			So(cmd.Result.Type, ShouldEqual, "email")
 			So(cmd.Result.Type, ShouldEqual, "email")
 			So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
 			So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
+			So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
 
 
 			Convey("Cannot save Alert Notification with the same name", func() {
 			Convey("Cannot save Alert Notification with the same name", func() {
 				err = CreateAlertNotificationCommand(cmd)
 				err = CreateAlertNotificationCommand(cmd)
@@ -227,18 +228,20 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
 
 
 			Convey("Can update alert notification", func() {
 			Convey("Can update alert notification", func() {
 				newCmd := &models.UpdateAlertNotificationCommand{
 				newCmd := &models.UpdateAlertNotificationCommand{
-					Name:         "NewName",
-					Type:         "webhook",
-					OrgId:        cmd.Result.OrgId,
-					SendReminder: true,
-					Frequency:    "60s",
-					Settings:     simplejson.New(),
-					Id:           cmd.Result.Id,
+					Name:                  "NewName",
+					Type:                  "webhook",
+					OrgId:                 cmd.Result.OrgId,
+					SendReminder:          true,
+					DisableResolveMessage: true,
+					Frequency:             "60s",
+					Settings:              simplejson.New(),
+					Id:                    cmd.Result.Id,
 				}
 				}
 				err := UpdateAlertNotification(newCmd)
 				err := UpdateAlertNotification(newCmd)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(newCmd.Result.Name, ShouldEqual, "NewName")
 				So(newCmd.Result.Name, ShouldEqual, "NewName")
 				So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
 				So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
+				So(newCmd.Result.DisableResolveMessage, ShouldBeTrue)
 			})
 			})
 
 
 			Convey("Can update alert notification to disable sending of reminders", func() {
 			Convey("Can update alert notification to disable sending of reminders", func() {

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

@@ -71,6 +71,9 @@ func addAlertMigrations(mg *Migrator) {
 	mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{
 	mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{
 		Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0",
 		Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0",
 	}))
 	}))
+	mg.AddMigration("Add column disable_resolve_message", NewAddColumnMigration(alert_notification, &Column{
+		Name: "disable_resolve_message", Type: DB_Bool, Nullable: false, Default: "0",
+	}))
 
 
 	mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
 	mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
 
 

+ 29 - 2
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -234,10 +234,37 @@ func parseMultiSelectValue(input string) []string {
 // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
 // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
 func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
 func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
 	regions := []string{
 	regions := []string{
-		"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1",
-		"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", "us-isob-east-1", "us-iso-east-1",
+		"ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "ca-central-1",
+		"eu-central-1", "eu-north-1", "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2",
+		"cn-north-1", "cn-northwest-1", "us-gov-east-1", "us-gov-west-1", "us-isob-east-1", "us-iso-east-1",
 	}
 	}
 
 
+	err := e.ensureClientSession("us-east-1")
+	if err != nil {
+		return nil, err
+	}
+	r, err := e.ec2Svc.DescribeRegions(&ec2.DescribeRegionsInput{})
+	if err != nil {
+		// ignore error for backward compatibility
+		plog.Error("Failed to get regions", "error", err)
+	} else {
+		for _, region := range r.Regions {
+			exists := false
+
+			for _, existingRegion := range regions {
+				if existingRegion == *region.RegionName {
+					exists = true
+					break
+				}
+			}
+
+			if !exists {
+				regions = append(regions, *region.RegionName)
+			}
+		}
+	}
+	sort.Strings(regions)
+
 	result := make([]suggestData, 0)
 	result := make([]suggestData, 0)
 	for _, region := range regions {
 	for _, region := range regions {
 		result = append(result, suggestData{Text: region, Value: region})
 		result = append(result, suggestData{Text: region, Value: region})

+ 1 - 0
public/app/features/alerting/NotificationsEditCtrl.ts

@@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl {
   defaults: any = {
   defaults: any = {
     type: 'email',
     type: 'email',
     sendReminder: false,
     sendReminder: false,
+    disableResolveMessage: false,
     frequency: '15m',
     frequency: '15m',
     settings: {
     settings: {
       httpMethod: 'POST',
       httpMethod: 'POST',

+ 10 - 3
public/app/features/alerting/partials/notification_edit.html

@@ -21,21 +21,28 @@
       <gf-form-switch
       <gf-form-switch
           class="gf-form"
           class="gf-form"
           label="Send on all alerts"
           label="Send on all alerts"
-          label-class="width-12"
+          label-class="width-14"
           checked="ctrl.model.isDefault"
           checked="ctrl.model.isDefault"
           tooltip="Use this notification for all alerts">
           tooltip="Use this notification for all alerts">
       </gf-form-switch>
       </gf-form-switch>
       <gf-form-switch
       <gf-form-switch
           class="gf-form"
           class="gf-form"
           label="Include image"
           label="Include image"
-          label-class="width-12"
+          label-class="width-14"
           checked="ctrl.model.settings.uploadImage"
           checked="ctrl.model.settings.uploadImage"
           tooltip="Captures an image and include it in the notification">
           tooltip="Captures an image and include it in the notification">
       </gf-form-switch>
       </gf-form-switch>
+      <gf-form-switch
+          class="gf-form"
+          label="Disable Resolve Message"
+          label-class="width-14"
+          checked="ctrl.model.disableResolveMessage"
+          tooltip="Disable the resolve message [OK] that is sent when alerting state returns to false">
+      </gf-form-switch>
       <gf-form-switch
       <gf-form-switch
           class="gf-form"
           class="gf-form"
           label="Send reminders"
           label="Send reminders"
-          label-class="width-12"
+          label-class="width-14"
           checked="ctrl.model.sendReminder"
           checked="ctrl.model.sendReminder"
           tooltip="Send additional notifications for triggered alerts">
           tooltip="Send additional notifications for triggered alerts">
       </gf-form-switch>
       </gf-form-switch>

+ 33 - 3
public/app/features/explore/PromQueryField.test.tsx

@@ -96,11 +96,14 @@ describe('PromQueryField typeahead handling', () => {
 
 
     it('returns label suggestions on label context but leaves out labels that already exist', () => {
     it('returns label suggestions on label context but leaves out labels that already exist', () => {
       const instance = shallow(
       const instance = shallow(
-        <PromQueryField {...defaultProps} labelKeys={{ '{job="foo"}': ['bar', 'job'] }} />
+        <PromQueryField
+          {...defaultProps}
+          labelKeys={{ '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] }}
+        />
       ).instance() as PromQueryField;
       ).instance() as PromQueryField;
-      const value = Plain.deserialize('{job="foo",}');
+      const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
       const range = value.selection.merge({
       const range = value.selection.merge({
-        anchorOffset: 11,
+        anchorOffset: 36,
       });
       });
       const valueWithSelection = value.change().select(range).value;
       const valueWithSelection = value.change().select(range).value;
       const result = instance.getTypeahead({
       const result = instance.getTypeahead({
@@ -113,6 +116,33 @@ describe('PromQueryField typeahead handling', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
     });
     });
 
 
+    it('returns label value suggestions inside a label value context after a negated matching operator', () => {
+      const instance = shallow(
+        <PromQueryField
+          {...defaultProps}
+          labelKeys={{ '{}': ['label'] }}
+          labelValues={{ '{}': { label: ['a', 'b', 'c'] } }}
+        />
+      ).instance() as PromQueryField;
+      const value = Plain.deserialize('{label!=}');
+      const range = value.selection.merge({ anchorOffset: 8 });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.getTypeahead({
+        text: '!=',
+        prefix: '',
+        wrapperClasses: ['context-labels'],
+        labelKey: 'label',
+        value: valueWithSelection,
+      });
+      expect(result.context).toBe('context-label-values');
+      expect(result.suggestions).toEqual([
+        {
+          items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
+          label: 'Label values for "label"',
+        },
+      ]);
+    });
+
     it('returns a refresher on label context and unavailable metric', () => {
     it('returns a refresher on label context and unavailable metric', () => {
       const instance = shallow(
       const instance = shallow(
         <PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />
         <PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />

+ 2 - 2
public/app/features/explore/PromQueryField.tsx

@@ -111,7 +111,7 @@ export function willApplySuggestion(
 
 
     case 'context-label-values': {
     case 'context-label-values': {
       // Always add quotes and remove existing ones instead
       // Always add quotes and remove existing ones instead
-      if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) {
+      if (!typeaheadText.match(/^(!?=~?"|")/)) {
         suggestion = `"${suggestion}`;
         suggestion = `"${suggestion}`;
       }
       }
       if (getNextCharacter() !== '"') {
       if (getNextCharacter() !== '"') {
@@ -421,7 +421,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     const containsMetric = selector.indexOf('__name__=') > -1;
     const containsMetric = selector.indexOf('__name__=') > -1;
     const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
     const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
 
 
-    if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
+    if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
       // Label values
       // Label values
       if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) {
       if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) {
         const labelValues = this.state.labelValues[selector][labelKey];
         const labelValues = this.state.labelValues[selector][labelKey];

+ 7 - 1
public/app/features/explore/QueryField.tsx

@@ -228,7 +228,13 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
       const offset = range.startOffset;
       const offset = range.startOffset;
       const text = selection.anchorNode.textContent;
       const text = selection.anchorNode.textContent;
       let prefix = text.substr(0, offset);
       let prefix = text.substr(0, offset);
-      if (cleanText) {
+
+      // Label values could have valid characters erased if `cleanText()` is
+      // blindly applied, which would undesirably interfere with suggestions
+      const labelValueMatch = prefix.match(/(?:!?=~?"?|")(.*)/);
+      if (labelValueMatch) {
+        prefix = labelValueMatch[1];
+      } else if (cleanText) {
         prefix = cleanText(prefix);
         prefix = cleanText(prefix);
       }
       }
 
 

+ 5 - 8
public/app/features/explore/utils/prometheus.ts

@@ -28,7 +28,7 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
 
 
 // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
 // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
 const selectorRegexp = /\{[^}]*?\}/;
 const selectorRegexp = /\{[^}]*?\}/;
-const labelRegexp = /\b\w+="[^"\n]*?"/g;
+const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
 export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
 export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
   if (!query.match(selectorRegexp)) {
   if (!query.match(selectorRegexp)) {
     // Special matcher for metrics
     // Special matcher for metrics
@@ -66,11 +66,8 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any
   // Extract clean labels to form clean selector, incomplete labels are dropped
   // Extract clean labels to form clean selector, incomplete labels are dropped
   const selector = query.slice(prefixOpen, suffixClose);
   const selector = query.slice(prefixOpen, suffixClose);
   const labels = {};
   const labels = {};
-  selector.replace(labelRegexp, match => {
-    const delimiterIndex = match.indexOf('=');
-    const key = match.slice(0, delimiterIndex);
-    const value = match.slice(delimiterIndex + 1, match.length);
-    labels[key] = value;
+  selector.replace(labelRegexp, (_, key, operator, value) => {
+    labels[key] = { value, operator };
     return '';
     return '';
   });
   });
 
 
@@ -78,12 +75,12 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any
   const metricPrefix = query.slice(0, prefixOpen);
   const metricPrefix = query.slice(0, prefixOpen);
   const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/);
   const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/);
   if (metricMatch) {
   if (metricMatch) {
-    labels['__name__'] = `"${metricMatch[0]}"`;
+    labels['__name__'] = { value: `"${metricMatch[0]}"`, operator: '=' };
   }
   }
 
 
   // Build sorted selector
   // Build sorted selector
   const labelKeys = Object.keys(labels).sort();
   const labelKeys = Object.keys(labels).sort();
-  const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(',');
+  const cleanSelector = labelKeys.map(key => `${key}${labels[key].operator}${labels[key].value}`).join(',');
 
 
   const selectorString = ['{', cleanSelector, '}'].join('');
   const selectorString = ['{', cleanSelector, '}'].join('');
 
 

+ 48 - 1
public/app/plugins/datasource/cloudwatch/config_ctrl.ts

@@ -1,17 +1,21 @@
+import _ from 'lodash';
 export class CloudWatchConfigCtrl {
 export class CloudWatchConfigCtrl {
   static templateUrl = 'partials/config.html';
   static templateUrl = 'partials/config.html';
   current: any;
   current: any;
+  datasourceSrv: any;
 
 
   accessKeyExist = false;
   accessKeyExist = false;
   secretKeyExist = false;
   secretKeyExist = false;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor($scope) {
+  constructor($scope, datasourceSrv) {
     this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp';
     this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp';
     this.current.jsonData.authType = this.current.jsonData.authType || 'credentials';
     this.current.jsonData.authType = this.current.jsonData.authType || 'credentials';
 
 
     this.accessKeyExist = this.current.secureJsonFields.accessKey;
     this.accessKeyExist = this.current.secureJsonFields.accessKey;
     this.secretKeyExist = this.current.secureJsonFields.secretKey;
     this.secretKeyExist = this.current.secureJsonFields.secretKey;
+    this.datasourceSrv = datasourceSrv;
+    this.getRegions();
   }
   }
 
 
   resetAccessKey() {
   resetAccessKey() {
@@ -36,4 +40,47 @@ export class CloudWatchConfigCtrl {
     { name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' },
     { name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' },
     { name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' },
     { name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' },
   ];
   ];
+
+  regions = [
+    'ap-northeast-1',
+    'ap-northeast-2',
+    'ap-northeast-3',
+    'ap-south-1',
+    'ap-southeast-1',
+    'ap-southeast-2',
+    'ca-central-1',
+    'cn-north-1',
+    'cn-northwest-1',
+    'eu-central-1',
+    'eu-north-1',
+    'eu-west-1',
+    'eu-west-2',
+    'eu-west-3',
+    'me-south-1',
+    'sa-east-1',
+    'us-east-1',
+    'us-east-2',
+    'us-gov-east-1',
+    'us-gov-west-1',
+    'us-iso-east-1',
+    'us-isob-east-1',
+    'us-west-1',
+    'us-west-2',
+  ];
+
+  getRegions() {
+    this.datasourceSrv
+      .loadDatasource(this.current.name)
+      .then(ds => {
+        return ds.getRegions();
+      })
+      .then(
+        regions => {
+          this.regions = _.map(regions, 'value');
+        },
+        err => {
+          console.error('failed to get latest regions');
+        }
+      );
+  }
 }
 }

+ 1 - 1
public/app/plugins/datasource/cloudwatch/partials/config.html

@@ -39,7 +39,7 @@
   <div class="gf-form">
   <div class="gf-form">
     <label class="gf-form-label width-13">Default Region</label>
     <label class="gf-form-label width-13">Default Region</label>
     <div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
     <div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
-      <select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2', 'us-isob-east-1', 'us-iso-east-1']"></select>
+      <select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ctrl.regions"></select>
       <info-popover mode="right-absolute">
       <info-popover mode="right-absolute">
         Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
         Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
       </info-popover>
       </info-popover>

+ 2 - 2
public/app/plugins/datasource/postgres/datasource.ts

@@ -20,7 +20,7 @@ export class PostgresDatasource {
     this.interval = (instanceSettings.jsonData || {}).timeInterval;
     this.interval = (instanceSettings.jsonData || {}).timeInterval;
   }
   }
 
 
-  interpolateVariable(value, variable) {
+  interpolateVariable = (value, variable) => {
     if (typeof value === 'string') {
     if (typeof value === 'string') {
       if (variable.multi || variable.includeAll) {
       if (variable.multi || variable.includeAll) {
         return this.queryModel.quoteLiteral(value);
         return this.queryModel.quoteLiteral(value);
@@ -37,7 +37,7 @@ export class PostgresDatasource {
       return this.queryModel.quoteLiteral(v);
       return this.queryModel.quoteLiteral(v);
     });
     });
     return quotedValues.join(',');
     return quotedValues.join(',');
-  }
+  };
 
 
   query(options) {
   query(options) {
     const queries = _.filter(options.targets, target => {
     const queries = _.filter(options.targets, target => {

+ 10 - 7
tools/phantomjs/render.js

@@ -50,19 +50,22 @@
 
 
       function checkIsReady() {
       function checkIsReady() {
         var panelsRendered = page.evaluate(function() {
         var panelsRendered = page.evaluate(function() {
-          var panelCount = document.querySelectorAll('.panel').length;
+          var panelCount = document.querySelectorAll('plugin-component').length;
           return window.panelsRendered >= panelCount;
           return window.panelsRendered >= panelCount;
         });
         });
 
 
         if (panelsRendered || totalWaitMs > timeoutMs) {
         if (panelsRendered || totalWaitMs > timeoutMs) {
           var bb = page.evaluate(function () {
           var bb = page.evaluate(function () {
-            return document.getElementsByClassName("main-view")[0].getBoundingClientRect();
+            var container = document.getElementsByClassName("dashboard-container")
+            if (container.length == 0) {
+               container = document.getElementsByClassName("panel-container")
+            }
+            return container[0].getBoundingClientRect();
           });
           });
-
-          page.clipRect = {
-            top:    bb.top,
-            left:   bb.left,
-            width:  bb.width,
+          
+          // reset viewport to render full page
+          page.viewportSize = {
+            width: bb.width,
             height: bb.height
             height: bb.height
           };
           };