diff --git a/CHANGELOG.md b/CHANGELOG.md index 1189b5fe112..319e80aff21 100644 --- a/CHANGELOG.md +++ b/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) * **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 -* **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) +* **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 * 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) * **Render**: Fix PhantomJS render of graph panel when legend displayed as table to the right [#13616](https://github.com/grafana/grafana/issues/13616) diff --git a/README.md b/README.md index 1179385d10c..c07cced6684 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download) ### Dependencies -- Go 1.11 +- Go (Latest Stable) - NodeJS LTS ### Building the backend diff --git a/UPGRADING_DEPENDENCIES.md b/UPGRADING_DEPENDENCIES.md new file mode 100644 index 00000000000..f3d2adbd71a --- /dev/null +++ b/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. \ No newline at end of file diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index 7adc6ebe4fb..be36d108475 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/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 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. Here is a minimal policy example: @@ -65,11 +65,12 @@ Here is a minimal policy example: "Resource": "*" }, { - "Sid": "AllowReadingTagsFromEC2", + "Sid": "AllowReadingTagsInstancesRegionsFromEC2", "Effect": "Allow", "Action": [ "ec2:DescribeTags", - "ec2:DescribeInstances" + "ec2:DescribeInstances", + "ec2:DescribeRegions" ], "Resource": "*" } diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index ba0d6199ba4..52353ede8c2 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -87,7 +87,7 @@ docker run \ ## 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: ```bash @@ -103,6 +103,21 @@ docker run \ 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=;` + +```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 ```bash diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index ea75b9797e8..eed05f05fa6 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple ## Dependencies -- [Go 1.11](https://golang.org/dl/) +- [Go (Latest Stable)](https://golang.org/dl/) - [Git](https://git-scm.com/downloads) - [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. diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index e9eb78fbe13..e7614614076 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -17,24 +17,8 @@ func GetDataSources(c *m.ReqContext) Response { 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) - for _, ds := range datasources { + for _, ds := range query.Result { dsItem := dtos.DataSourceListItemDTO{ OrgId: ds.OrgId, Id: ds.Id, diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 697d0a35a08..c037831f341 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -49,28 +49,30 @@ func formatShort(interval time.Duration) string { func NewAlertNotification(notification *models.AlertNotification) *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 { - 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 { @@ -100,11 +102,12 @@ type EvalMatch 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 { diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 2128b469fa4..e0fd12937ed 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -23,38 +23,41 @@ var ( ) 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 { - 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:"-"` Result *AlertNotification } 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:"-"` Result *AlertNotification diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 488fb2fe1fa..b71d17ec0d1 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -195,8 +195,8 @@ type GetDataSourceByNameQuery struct { type DsPermissionType int const ( - DsPermissionQuery DsPermissionType = 1 << iota - DsPermissionNoAccess + DsPermissionNoAccess DsPermissionType = iota + DsPermissionQuery ) func (p DsPermissionType) String() string { @@ -207,12 +207,6 @@ func (p DsPermissionType) String() string { return names[int(p)] } -type HasRequiredDataSourcePermissionQuery struct { - Id int64 - User *SignedInUser - RequiredPermission DsPermissionType -} - type GetDataSourcePermissionsForUserQuery struct { User *SignedInUser Result map[int64]DsPermissionType diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 96294f0624f..040d0991861 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -27,6 +27,7 @@ type Notifier interface { GetNotifierId() int64 GetIsDefault() bool GetSendReminder() bool + GetDisableResolveMessage() bool GetFrequency() time.Duration } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index fbade2eccac..d141d6cd257 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting" ) @@ -15,13 +14,14 @@ const ( ) 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 } @@ -34,14 +34,15 @@ func NewNotifierBase(model *models.AlertNotification) 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 } @@ -106,6 +112,10 @@ func (n *NotifierBase) GetSendReminder() bool { return n.SendReminder } +func (n *NotifierBase) GetDisableResolveMessage() bool { + return n.DisableResolveMessage +} + func (n *NotifierBase) GetFrequency() time.Duration { return n.Frequency } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5e46d3ad72e..5062828cb4f 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -179,5 +179,10 @@ func TestBaseNotifier(t *testing.T) { base := NewNotifierBase(model) So(base.UploadImage, ShouldBeTrue) }) + + Convey("default value should be false for backwards compatibility", func() { + base := NewNotifierBase(model) + So(base.DisableResolveMessage, ShouldBeFalse) + }) }) } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index daaef945b96..afe6269510f 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -66,6 +66,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro alert_notification.updated, alert_notification.settings, alert_notification.is_default, + alert_notification.disable_resolve_message, alert_notification.send_reminder, alert_notification.frequency FROM alert_notification @@ -106,6 +107,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS alert_notification.updated, alert_notification.settings, alert_notification.is_default, + alert_notification.disable_resolve_message, alert_notification.send_reminder, alert_notification.frequency FROM alert_notification @@ -166,15 +168,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } 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 { @@ -210,6 +213,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Type = cmd.Type current.IsDefault = cmd.IsDefault current.SendReminder = cmd.SendReminder + current.DisableResolveMessage = cmd.DisableResolveMessage if current.SendReminder { if cmd.Frequency == "" { @@ -224,7 +228,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { 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 { return err diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index ed682bae5c6..629a6292eb5 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/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.Type, ShouldEqual, "email") So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) + So(cmd.Result.DisableResolveMessage, ShouldBeFalse) Convey("Cannot save Alert Notification with the same name", func() { err = CreateAlertNotificationCommand(cmd) @@ -227,18 +228,20 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can update alert notification", func() { 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) So(err, ShouldBeNil) So(newCmd.Result.Name, ShouldEqual, "NewName") So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second) + So(newCmd.Result.DisableResolveMessage, ShouldBeTrue) }) Convey("Can update alert notification to disable sending of reminders", func() { diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index cadcccf6c95..198a47b50ff 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/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{ 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])) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index ee9d9583c4e..b74af76f09a 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/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 func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) { 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) for _, region := range regions { result = append(result, suggestData{Text: region, Value: region}) diff --git a/public/app/features/alerting/NotificationsEditCtrl.ts b/public/app/features/alerting/NotificationsEditCtrl.ts index 315a9a619a1..2607121bb0e 100644 --- a/public/app/features/alerting/NotificationsEditCtrl.ts +++ b/public/app/features/alerting/NotificationsEditCtrl.ts @@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl { defaults: any = { type: 'email', sendReminder: false, + disableResolveMessage: false, frequency: '15m', settings: { httpMethod: 'POST', diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 7b198736b83..b2cd2f21e4d 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -21,21 +21,28 @@ + + diff --git a/public/app/features/explore/PromQueryField.test.tsx b/public/app/features/explore/PromQueryField.test.tsx index c82a1cd448f..802bb695f2c 100644 --- a/public/app/features/explore/PromQueryField.test.tsx +++ b/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', () => { const instance = shallow( - + ).instance() as PromQueryField; - const value = Plain.deserialize('{job="foo",}'); + const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}'); const range = value.selection.merge({ - anchorOffset: 11, + anchorOffset: 36, }); const valueWithSelection = value.change().select(range).value; const result = instance.getTypeahead({ @@ -113,6 +116,33 @@ describe('PromQueryField typeahead handling', () => { 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( + + ).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', () => { const instance = shallow( diff --git a/public/app/features/explore/PromQueryField.tsx b/public/app/features/explore/PromQueryField.tsx index fbe58912a81..442e51af987 100644 --- a/public/app/features/explore/PromQueryField.tsx +++ b/public/app/features/explore/PromQueryField.tsx @@ -111,7 +111,7 @@ export function willApplySuggestion( case 'context-label-values': { // Always add quotes and remove existing ones instead - if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) { + if (!typeaheadText.match(/^(!?=~?"|")/)) { suggestion = `"${suggestion}`; } if (getNextCharacter() !== '"') { @@ -421,7 +421,7 @@ class PromQueryField extends React.PureComponent -1; const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; - if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { + if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) { // Label values if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) { const labelValues = this.state.labelValues[selector][labelKey]; diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index c89893b4f28..c3c41b7ab17 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -228,7 +228,13 @@ class QueryField extends React.PureComponent s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; 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 } { if (!query.match(selectorRegexp)) { // 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 const selector = query.slice(prefixOpen, suffixClose); 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 ''; }); @@ -78,12 +75,12 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any const metricPrefix = query.slice(0, prefixOpen); const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/); if (metricMatch) { - labels['__name__'] = `"${metricMatch[0]}"`; + labels['__name__'] = { value: `"${metricMatch[0]}"`, operator: '=' }; } // Build sorted selector 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(''); diff --git a/public/app/plugins/datasource/cloudwatch/config_ctrl.ts b/public/app/plugins/datasource/cloudwatch/config_ctrl.ts index ff0d39944ca..6fe48cb1715 100644 --- a/public/app/plugins/datasource/cloudwatch/config_ctrl.ts +++ b/public/app/plugins/datasource/cloudwatch/config_ctrl.ts @@ -1,17 +1,21 @@ +import _ from 'lodash'; export class CloudWatchConfigCtrl { static templateUrl = 'partials/config.html'; current: any; + datasourceSrv: any; accessKeyExist = false; secretKeyExist = false; /** @ngInject */ - constructor($scope) { + constructor($scope, datasourceSrv) { this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp'; this.current.jsonData.authType = this.current.jsonData.authType || 'credentials'; this.accessKeyExist = this.current.secureJsonFields.accessKey; this.secretKeyExist = this.current.secureJsonFields.secretKey; + this.datasourceSrv = datasourceSrv; + this.getRegions(); } resetAccessKey() { @@ -36,4 +40,47 @@ export class CloudWatchConfigCtrl { { name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' }, { 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'); + } + ); + } } diff --git a/public/app/plugins/datasource/cloudwatch/partials/config.html b/public/app/plugins/datasource/cloudwatch/partials/config.html index e5ab0910cba..40249d32b7e 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/config.html +++ b/public/app/plugins/datasource/cloudwatch/partials/config.html @@ -39,7 +39,7 @@
- + Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region. diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index f1db05cabe8..13948c5d793 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -20,7 +20,7 @@ export class PostgresDatasource { this.interval = (instanceSettings.jsonData || {}).timeInterval; } - interpolateVariable(value, variable) { + interpolateVariable = (value, variable) => { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { return this.queryModel.quoteLiteral(value); @@ -37,7 +37,7 @@ export class PostgresDatasource { return this.queryModel.quoteLiteral(v); }); return quotedValues.join(','); - } + }; query(options) { const queries = _.filter(options.targets, target => { diff --git a/tools/phantomjs/render.js b/tools/phantomjs/render.js index 900660b7ca0..d0fb0234417 100644 --- a/tools/phantomjs/render.js +++ b/tools/phantomjs/render.js @@ -50,19 +50,22 @@ function checkIsReady() { var panelsRendered = page.evaluate(function() { - var panelCount = document.querySelectorAll('.panel').length; + var panelCount = document.querySelectorAll('plugin-component').length; return window.panelsRendered >= panelCount; }); if (panelsRendered || totalWaitMs > timeoutMs) { 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 };