Commit 02769b6d by Peter Holmberg

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

parents 84bbfe11 1d60c45a
...@@ -4,16 +4,22 @@ ...@@ -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)
......
...@@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download) ...@@ -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
......
# 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.
\ No newline at end of file
...@@ -46,7 +46,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu ...@@ -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: ...@@ -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": "*"
} }
......
...@@ -87,7 +87,7 @@ docker run \ ...@@ -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 \ ...@@ -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
......
...@@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple ...@@ -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.
......
...@@ -17,24 +17,8 @@ func GetDataSources(c *m.ReqContext) Response { ...@@ -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,
......
...@@ -49,28 +49,30 @@ func formatShort(interval time.Duration) string { ...@@ -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, Id: notification.Id,
Name: notification.Name, Name: notification.Name,
Type: notification.Type, Type: notification.Type,
IsDefault: notification.IsDefault, IsDefault: notification.IsDefault,
Created: notification.Created, Created: notification.Created,
Updated: notification.Updated, Updated: notification.Updated,
Frequency: formatShort(notification.Frequency), Frequency: formatShort(notification.Frequency),
SendReminder: notification.SendReminder, SendReminder: notification.SendReminder,
Settings: notification.Settings, DisableResolveMessage: notification.DisableResolveMessage,
Settings: notification.Settings,
} }
} }
type AlertNotification struct { type AlertNotification struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
SendReminder bool `json:"sendReminder"` SendReminder bool `json:"sendReminder"`
Frequency string `json:"frequency"` DisableResolveMessage bool `json:"disableResolveMessage"`
Created time.Time `json:"created"` Frequency string `json:"frequency"`
Updated time.Time `json:"updated"` Created time.Time `json:"created"`
Settings *simplejson.Json `json:"settings"` Updated time.Time `json:"updated"`
Settings *simplejson.Json `json:"settings"`
} }
type AlertTestCommand struct { type AlertTestCommand struct {
...@@ -100,11 +102,12 @@ type EvalMatch struct { ...@@ -100,11 +102,12 @@ type EvalMatch struct {
} }
type NotificationTestCommand struct { type NotificationTestCommand struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
SendReminder bool `json:"sendReminder"` SendReminder bool `json:"sendReminder"`
Frequency string `json:"frequency"` DisableResolveMessage bool `json:"disableResolveMessage"`
Settings *simplejson.Json `json:"settings"` Frequency string `json:"frequency"`
Settings *simplejson.Json `json:"settings"`
} }
type PauseAlertCommand struct { type PauseAlertCommand struct {
......
...@@ -23,38 +23,41 @@ var ( ...@@ -23,38 +23,41 @@ var (
) )
type AlertNotification struct { type AlertNotification struct {
Id int64 `json:"id"` Id int64 `json:"id"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
SendReminder bool `json:"sendReminder"` SendReminder bool `json:"sendReminder"`
Frequency time.Duration `json:"frequency"` DisableResolveMessage bool `json:"disableResolveMessage"`
IsDefault bool `json:"isDefault"` Frequency time.Duration `json:"frequency"`
Settings *simplejson.Json `json:"settings"` IsDefault bool `json:"isDefault"`
Created time.Time `json:"created"` Settings *simplejson.Json `json:"settings"`
Updated time.Time `json:"updated"` Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
} }
type CreateAlertNotificationCommand struct { type CreateAlertNotificationCommand struct {
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"` Type string `json:"type" binding:"Required"`
SendReminder bool `json:"sendReminder"` SendReminder bool `json:"sendReminder"`
Frequency string `json:"frequency"` DisableResolveMessage bool `json:"disableResolveMessage"`
IsDefault bool `json:"isDefault"` Frequency string `json:"frequency"`
Settings *simplejson.Json `json:"settings"` 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"` Id int64 `json:"id" binding:"Required"`
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"` Type string `json:"type" binding:"Required"`
SendReminder bool `json:"sendReminder"` SendReminder bool `json:"sendReminder"`
Frequency string `json:"frequency"` DisableResolveMessage bool `json:"disableResolveMessage"`
IsDefault bool `json:"isDefault"` Frequency string `json:"frequency"`
Settings *simplejson.Json `json:"settings" binding:"Required"` IsDefault bool `json:"isDefault"`
Settings *simplejson.Json `json:"settings" binding:"Required"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
Result *AlertNotification Result *AlertNotification
......
...@@ -195,8 +195,8 @@ type GetDataSourceByNameQuery struct { ...@@ -195,8 +195,8 @@ type GetDataSourceByNameQuery struct {
type DsPermissionType int type DsPermissionType int
const ( const (
DsPermissionQuery DsPermissionType = 1 << iota DsPermissionNoAccess DsPermissionType = iota
DsPermissionNoAccess DsPermissionQuery
) )
func (p DsPermissionType) String() string { func (p DsPermissionType) String() string {
...@@ -207,12 +207,6 @@ 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
......
...@@ -27,6 +27,7 @@ type Notifier interface { ...@@ -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
} }
......
...@@ -6,7 +6,6 @@ import ( ...@@ -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 ( ...@@ -15,13 +14,14 @@ const (
) )
type NotifierBase struct { type NotifierBase struct {
Name string Name string
Type string Type string
Id int64 Id int64
IsDeault bool IsDeault bool
UploadImage bool UploadImage bool
SendReminder bool SendReminder bool
Frequency time.Duration DisableResolveMessage bool
Frequency time.Duration
log log.Logger log log.Logger
} }
...@@ -34,14 +34,15 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { ...@@ -34,14 +34,15 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
} }
return NotifierBase{ return NotifierBase{
Id: model.Id, Id: model.Id,
Name: model.Name, Name: model.Name,
IsDeault: model.IsDefault, IsDeault: model.IsDefault,
Type: model.Type, Type: model.Type,
UploadImage: uploadImage, UploadImage: uploadImage,
SendReminder: model.SendReminder, SendReminder: model.SendReminder,
Frequency: model.Frequency, DisableResolveMessage: model.DisableResolveMessage,
log: log.New("alerting.notifier." + model.Name), Frequency: model.Frequency,
log: log.New("alerting.notifier." + model.Name),
} }
} }
...@@ -83,6 +84,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC ...@@ -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 { ...@@ -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
} }
...@@ -179,5 +179,10 @@ func TestBaseNotifier(t *testing.T) { ...@@ -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)
})
}) })
} }
...@@ -66,6 +66,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro ...@@ -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 ...@@ -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 ...@@ -166,15 +168,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
} }
alertNotification := &m.AlertNotification{ alertNotification := &m.AlertNotification{
OrgId: cmd.OrgId, OrgId: cmd.OrgId,
Name: cmd.Name, Name: cmd.Name,
Type: cmd.Type, Type: cmd.Type,
Settings: cmd.Settings, Settings: cmd.Settings,
SendReminder: cmd.SendReminder, SendReminder: cmd.SendReminder,
Frequency: frequency, DisableResolveMessage: cmd.DisableResolveMessage,
Created: time.Now(), Frequency: frequency,
Updated: time.Now(), Created: time.Now(),
IsDefault: cmd.IsDefault, 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 { ...@@ -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 { ...@@ -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
......
...@@ -219,6 +219,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { ...@@ -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) { ...@@ -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", Name: "NewName",
Type: "webhook", Type: "webhook",
OrgId: cmd.Result.OrgId, OrgId: cmd.Result.OrgId,
SendReminder: true, SendReminder: true,
Frequency: "60s", DisableResolveMessage: true,
Settings: simplejson.New(), Frequency: "60s",
Id: cmd.Result.Id, 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() {
......
...@@ -71,6 +71,9 @@ func addAlertMigrations(mg *Migrator) { ...@@ -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]))
......
...@@ -234,10 +234,37 @@ func parseMultiSelectValue(input string) []string { ...@@ -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", "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-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", "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})
......
...@@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl { ...@@ -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',
......
...@@ -21,21 +21,28 @@ ...@@ -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 <gf-form-switch
class="gf-form" 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
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>
......
...@@ -96,11 +96,14 @@ describe('PromQueryField typeahead handling', () => { ...@@ -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', () => { ...@@ -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'] }} />
......
...@@ -111,7 +111,7 @@ export function willApplySuggestion( ...@@ -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 ...@@ -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];
......
...@@ -228,7 +228,13 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField ...@@ -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);
} }
......
...@@ -28,7 +28,7 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); ...@@ -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 ...@@ -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 => { selector.replace(labelRegexp, (_, key, operator, value) => {
const delimiterIndex = match.indexOf('='); labels[key] = { value, operator };
const key = match.slice(0, delimiterIndex);
const value = match.slice(delimiterIndex + 1, match.length);
labels[key] = value;
return ''; return '';
}); });
...@@ -78,12 +75,12 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any ...@@ -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('');
......
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 { ...@@ -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');
}
);
}
} }
...@@ -39,7 +39,7 @@ ...@@ -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>
......
...@@ -20,7 +20,7 @@ export class PostgresDatasource { ...@@ -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 { ...@@ -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 => {
......
...@@ -50,19 +50,22 @@ ...@@ -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 = { // reset viewport to render full page
top: bb.top, page.viewportSize = {
left: bb.left, width: bb.width,
width: bb.width,
height: bb.height height: bb.height
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment