Просмотр исходного кода

Merged with master, resolved conflicts

utkarshcmu 10 лет назад
Родитель
Сommit
c433167950
100 измененных файлов с 2522 добавлено и 491 удалено
  1. 22 1
      CHANGELOG.md
  2. 20 20
      Godeps/Godeps.json
  3. 19 7
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go
  4. 34 0
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value_test.go
  5. 13 4
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/client/client.go
  6. 22 0
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/config.go
  7. 11 3
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination.go
  8. 63 0
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination_test.go
  9. 8 0
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/retryer.go
  10. 1 1
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/version.go
  11. 64 31
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter.go
  12. 228 20
      Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter_test.go
  13. 16 1
      build.go
  14. 25 0
      conf/defaults.ini
  15. 14 0
      conf/sample.ini
  16. 1 1
      docs/VERSION
  17. 6 4
      docs/mkdocs.yml
  18. 29 0
      docs/sources/datasources/elasticsearch.md
  19. 38 18
      docs/sources/datasources/influxdb.md
  20. 123 0
      docs/sources/guides/whats-new-in-v2-6.md
  21. 3 3
      docs/sources/installation/debian.md
  22. 4 4
      docs/sources/installation/rpm.md
  23. 86 0
      docs/sources/reference/table_panel.md
  24. 1 0
      docs/sources/versions.html_fragment
  25. 1 1
      package.json
  26. 2 0
      packaging/deb/default/grafana-server
  27. 3 1
      packaging/deb/init.d/grafana-server
  28. 2 1
      packaging/deb/systemd/grafana-server.service
  29. 1 1
      packaging/publish/publish.sh
  30. 2 1
      packaging/rpm/init.d/grafana-server
  31. 2 0
      packaging/rpm/sysconfig/grafana-server
  32. 2 1
      packaging/rpm/systemd/grafana-server.service
  33. 8 4
      pkg/api/api.go
  34. 120 9
      pkg/api/cloudwatch/cloudwatch.go
  35. 43 22
      pkg/api/cloudwatch/metrics.go
  36. 8 0
      pkg/api/dashboard_snapshot.go
  37. 2 1
      pkg/api/datasources.go
  38. 25 0
      pkg/api/dtos/index.go
  39. 8 0
      pkg/api/dtos/plugin_bundle.go
  40. 13 0
      pkg/api/frontendsettings.go
  41. 44 35
      pkg/api/index.go
  42. 7 6
      pkg/api/login.go
  43. 15 8
      pkg/cmd/web.go
  44. 95 0
      pkg/log/syslog.go
  45. 1 1
      pkg/login/ldap.go
  46. 6 1
      pkg/middleware/logger.go
  47. 3 0
      pkg/models/datasource.go
  48. 34 0
      pkg/models/plugin_bundle.go
  49. 26 0
      pkg/plugins/models.go
  50. 66 18
      pkg/plugins/plugins.go
  51. 5 2
      pkg/plugins/plugins_test.go
  52. 2 0
      pkg/services/sqlstore/datasource.go
  53. 5 0
      pkg/services/sqlstore/migrations/datasource_mig.go
  54. 1 0
      pkg/services/sqlstore/migrations/migrations.go
  55. 26 0
      pkg/services/sqlstore/migrations/plugin_bundle.go
  56. 1 1
      pkg/services/sqlstore/migrator/column.go
  57. 6 1
      pkg/services/sqlstore/migrator/dialect.go
  58. 4 0
      pkg/services/sqlstore/migrator/migrations.go
  59. 11 0
      pkg/services/sqlstore/migrator/postgres_dialect.go
  60. 46 0
      pkg/services/sqlstore/plugin_bundle.go
  61. 29 7
      pkg/setting/setting.go
  62. 98 0
      pkg/util/filepath.go
  63. 11 6
      public/app/app.js
  64. 1 0
      public/app/core/config.js
  65. 1 0
      public/app/core/controllers/login_ctrl.js
  66. 6 11
      public/app/core/controllers/sidemenu_ctrl.js
  67. 1 1
      public/app/core/directives/value_select_dropdown.js
  68. 13 0
      public/app/core/routes/all.js
  69. 2 2
      public/app/core/services/context_srv.js
  70. 1 8
      public/app/core/settings.js
  71. 4 2
      public/app/core/utils/kbn.js
  72. 2 2
      public/app/core/utils/rangeutil.ts
  73. 26 24
      public/app/features/admin/partials/orgs.html
  74. 32 28
      public/app/features/admin/partials/users.html
  75. 6 1
      public/app/features/dashboard/dashboardCtrl.js
  76. 15 0
      public/app/features/dashboard/dashboardNavCtrl.js
  77. 9 4
      public/app/features/dashboard/dashboardSrv.js
  78. 1 0
      public/app/features/dashboard/partials/dashboardTopNav.html
  79. 282 0
      public/app/features/dashboard/partials/globalAlerts.html
  80. 3 4
      public/app/features/dashboard/partials/shareModal.html
  81. 10 1
      public/app/features/dashboard/rowCtrl.js
  82. 8 1
      public/app/features/dashboard/shareSnapshotCtrl.js
  83. 1 3
      public/app/features/dashboard/timepicker/timepicker.html
  84. 4 0
      public/app/features/org/all.js
  85. 52 41
      public/app/features/org/partials/datasourceHttpConfig.html
  86. 40 35
      public/app/features/org/partials/datasources.html
  87. 21 18
      public/app/features/org/partials/orgApiKeys.html
  88. 51 39
      public/app/features/org/partials/orgUsers.html
  89. 3 0
      public/app/features/org/partials/pluginConfigCore.html
  90. 42 0
      public/app/features/org/partials/pluginEdit.html
  91. 41 0
      public/app/features/org/partials/plugins.html
  92. 35 0
      public/app/features/org/pluginEditCtrl.js
  93. 47 0
      public/app/features/org/plugin_directive.js
  94. 58 0
      public/app/features/org/plugin_srv.js
  95. 33 0
      public/app/features/org/pluginsCtrl.js
  96. 2 2
      public/app/features/panel/panel_directive.js
  97. 8 2
      public/app/features/panel/panel_helper.js
  98. 24 15
      public/app/features/profile/partials/profile.html
  99. 1 1
      public/app/features/templating/partials/editor.html
  100. 1 0
      public/app/features/templating/templateSrv.js

+ 22 - 1
CHANGELOG.md

@@ -1,7 +1,28 @@
-# 2.6.0 (2015-12-04)
+# 3.0.0 (unrelased master branch)
+
+
+### Breaking changes
+**InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds. Can easily be installed via improved plugin system, closes #3523
+**KairosDB** The data source is no longer included in default builds. Can easily be installed via improved plugin system, closes #3524
+
+# 2.6.1 (unrelased, 2.6.x branch)
+
+### New Features
+* **Elasticsearch**: Support for derivative unit option, closes [#3512](https://github.com/grafana/grafana/issues/3512)
+
+# 2.6.0 (2015-12-14)
+
+### New Features
+* **Elasticsearch**: Support for pipeline aggregations Moving average and derivative, closes [#2715](https://github.com/grafana/grafana/issues/2715)
+* **Elasticsearch**: Support for inline script and missing options for metrics, closes [#3500](https://github.com/grafana/grafana/issues/3500)
+* **Syslog**: Support for syslog logging, closes [#3161](https://github.com/grafana/grafana/pull/3161)
+* **Timepicker**: Always show refresh button even with refresh rate, closes [#3498](https://github.com/grafana/grafana/pull/3498)
+* **Login**: Make it possible to change the login hint on the login page, closes [#2571](https://github.com/grafana/grafana/pull/2571)
 
 ### Bug Fixes
 * **metric editors**: Fix for clicking typeahead auto dropdown option, fixes [#3428](https://github.com/grafana/grafana/issues/3428)
+* **influxdb**: Fixed issue showing Group By label only on first query, fixes [#3453](https://github.com/grafana/grafana/issues/3453)
+* **logging**: Add more verbose info logging for http reqeusts, closes [#3405](https://github.com/grafana/grafana/pull/3405)
 
 # 2.6.0-Beta1 (2015-12-04)
 

+ 20 - 20
Godeps/Godeps.json

@@ -20,53 +20,53 @@
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/aws",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/private/endpoints",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/ec2query",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/private/signer/v4",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/private/waiter",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/service/cloudwatch",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/aws/aws-sdk-go/service/ec2",
-			"Comment": "v0.10.4-18-gce51895",
-			"Rev": "ce51895e994693d65ab997ae48032bf13a9290b7"
+			"Comment": "v1.0.0",
+			"Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f"
 		},
 		{
 			"ImportPath": "github.com/davecgh/go-spew/spew",

+ 19 - 7
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go

@@ -13,11 +13,11 @@ var indexRe = regexp.MustCompile(`(.+)\[(-?\d+)?\]$`)
 
 // rValuesAtPath returns a slice of values found in value v. The values
 // in v are explored recursively so all nested values are collected.
-func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool) []reflect.Value {
+func rValuesAtPath(v interface{}, path string, createPath, caseSensitive, nilTerm bool) []reflect.Value {
 	pathparts := strings.Split(path, "||")
 	if len(pathparts) > 1 {
 		for _, pathpart := range pathparts {
-			vals := rValuesAtPath(v, pathpart, create, caseSensitive)
+			vals := rValuesAtPath(v, pathpart, createPath, caseSensitive, nilTerm)
 			if len(vals) > 0 {
 				return vals
 			}
@@ -76,7 +76,16 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
 				return false
 			})
 
-			if create && value.Kind() == reflect.Ptr && value.IsNil() {
+			if nilTerm && value.Kind() == reflect.Ptr && len(components[1:]) == 0 {
+				if !value.IsNil() {
+					value.Set(reflect.Zero(value.Type()))
+				}
+				return []reflect.Value{value}
+			}
+
+			if createPath && value.Kind() == reflect.Ptr && value.IsNil() {
+				// TODO if the value is the terminus it should not be created
+				// if the value to be set to its position is nil.
 				value.Set(reflect.New(value.Type().Elem()))
 				value = value.Elem()
 			} else {
@@ -84,7 +93,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
 			}
 
 			if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
-				if !create && value.IsNil() {
+				if !createPath && value.IsNil() {
 					value = reflect.ValueOf(nil)
 				}
 			}
@@ -116,7 +125,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
 				// pull out index
 				i := int(*index)
 				if i >= value.Len() { // check out of bounds
-					if create {
+					if createPath {
 						// TODO resize slice
 					} else {
 						continue
@@ -127,7 +136,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool)
 				value = reflect.Indirect(value.Index(i))
 
 				if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
-					if !create && value.IsNil() {
+					if !createPath && value.IsNil() {
 						value = reflect.ValueOf(nil)
 					}
 				}
@@ -176,8 +185,11 @@ func ValuesAtPath(i interface{}, path string) ([]interface{}, error) {
 // SetValueAtPath sets a value at the case insensitive lexical path inside
 // of a structure.
 func SetValueAtPath(i interface{}, path string, v interface{}) {
-	if rvals := rValuesAtPath(i, path, true, false); rvals != nil {
+	if rvals := rValuesAtPath(i, path, true, false, v == nil); rvals != nil {
 		for _, rval := range rvals {
+			if rval.Kind() == reflect.Ptr && rval.IsNil() {
+				continue
+			}
 			setValue(rval, v)
 		}
 	}

+ 34 - 0
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value_test.go

@@ -105,4 +105,38 @@ func TestSetValueAtPathSuccess(t *testing.T) {
 	assert.Equal(t, "test0", s2.B.B.C)
 	awsutil.SetValueAtPath(&s2, "A", []Struct{{}})
 	assert.Equal(t, []Struct{{}}, s2.A)
+
+	str := "foo"
+
+	s3 := Struct{}
+	awsutil.SetValueAtPath(&s3, "b.b.c", str)
+	assert.Equal(t, "foo", s3.B.B.C)
+
+	s3 = Struct{B: &Struct{B: &Struct{C: str}}}
+	awsutil.SetValueAtPath(&s3, "b.b.c", nil)
+	assert.Equal(t, "", s3.B.B.C)
+
+	s3 = Struct{}
+	awsutil.SetValueAtPath(&s3, "b.b.c", nil)
+	assert.Equal(t, "", s3.B.B.C)
+
+	s3 = Struct{}
+	awsutil.SetValueAtPath(&s3, "b.b.c", &str)
+	assert.Equal(t, "foo", s3.B.B.C)
+
+	var s4 struct{ Name *string }
+	awsutil.SetValueAtPath(&s4, "Name", str)
+	assert.Equal(t, str, *s4.Name)
+
+	s4 = struct{ Name *string }{}
+	awsutil.SetValueAtPath(&s4, "Name", nil)
+	assert.Equal(t, (*string)(nil), s4.Name)
+
+	s4 = struct{ Name *string }{Name: &str}
+	awsutil.SetValueAtPath(&s4, "Name", nil)
+	assert.Equal(t, (*string)(nil), s4.Name)
+
+	s4 = struct{ Name *string }{}
+	awsutil.SetValueAtPath(&s4, "Name", &str)
+	assert.Equal(t, str, *s4.Name)
 }

+ 13 - 4
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/client/client.go

@@ -41,11 +41,20 @@ func New(cfg aws.Config, info metadata.ClientInfo, handlers request.Handlers, op
 		Handlers:   handlers,
 	}
 
-	maxRetries := aws.IntValue(cfg.MaxRetries)
-	if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
-		maxRetries = 3
+	switch retryer, ok := cfg.Retryer.(request.Retryer); {
+	case ok:
+		svc.Retryer = retryer
+	case cfg.Retryer != nil && cfg.Logger != nil:
+		s := fmt.Sprintf("WARNING: %T does not implement request.Retryer; using DefaultRetryer instead", cfg.Retryer)
+		cfg.Logger.Log(s)
+		fallthrough
+	default:
+		maxRetries := aws.IntValue(cfg.MaxRetries)
+		if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
+			maxRetries = 3
+		}
+		svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries}
 	}
-	svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries}
 
 	svc.AddDebugHandlers()
 

+ 22 - 0
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/config.go

@@ -12,6 +12,9 @@ import (
 // is nil also.
 const UseServiceDefaultRetries = -1
 
+// RequestRetryer is an alias for a type that implements the request.Retryer interface.
+type RequestRetryer interface{}
+
 // A Config provides service configuration for service clients. By default,
 // all clients will use the {defaults.DefaultConfig} structure.
 type Config struct {
@@ -59,6 +62,21 @@ type Config struct {
 	// configuration.
 	MaxRetries *int
 
+	// Retryer guides how HTTP requests should be retried in case of recoverable failures.
+	//
+	// When nil or the value does not implement the request.Retryer interface,
+	// the request.DefaultRetryer will be used.
+	//
+	// When both Retryer and MaxRetries are non-nil, the former is used and
+	// the latter ignored.
+	//
+	// To set the Retryer field in a type-safe manner and with chaining, use
+	// the request.WithRetryer helper function:
+	//
+	//   cfg := request.WithRetryer(aws.NewConfig(), myRetryer)
+	//
+	Retryer RequestRetryer
+
 	// Disables semantic parameter validation, which validates input for missing
 	// required fields and/or other semantic request input errors.
 	DisableParamValidation *bool
@@ -217,6 +235,10 @@ func mergeInConfig(dst *Config, other *Config) {
 		dst.MaxRetries = other.MaxRetries
 	}
 
+	if other.Retryer != nil {
+		dst.Retryer = other.Retryer
+	}
+
 	if other.DisableParamValidation != nil {
 		dst.DisableParamValidation = other.DisableParamValidation
 	}

+ 11 - 3
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination.go

@@ -44,12 +44,19 @@ func (r *Request) nextPageTokens() []interface{} {
 	}
 
 	tokens := []interface{}{}
+	tokenAdded := false
 	for _, outToken := range r.Operation.OutputTokens {
 		v, _ := awsutil.ValuesAtPath(r.Data, outToken)
 		if len(v) > 0 {
 			tokens = append(tokens, v[0])
+			tokenAdded = true
+		} else {
+			tokens = append(tokens, nil)
 		}
 	}
+	if !tokenAdded {
+		return nil
+	}
 
 	return tokens
 }
@@ -85,9 +92,10 @@ func (r *Request) NextPage() *Request {
 // return true to keep iterating or false to stop.
 func (r *Request) EachPage(fn func(data interface{}, isLastPage bool) (shouldContinue bool)) error {
 	for page := r; page != nil; page = page.NextPage() {
-		page.Send()
-		shouldContinue := fn(page.Data, !page.HasNextPage())
-		if page.Error != nil || !shouldContinue {
+		if err := page.Send(); err != nil {
+			return err
+		}
+		if getNextPage := fn(page.Data, !page.HasNextPage()); !getNextPage {
 			return page.Error
 		}
 	}

+ 63 - 0
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination_test.go

@@ -9,6 +9,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/awstesting/unit"
 	"github.com/aws/aws-sdk-go/service/dynamodb"
+	"github.com/aws/aws-sdk-go/service/route53"
 	"github.com/aws/aws-sdk-go/service/s3"
 )
 
@@ -314,7 +315,69 @@ func TestPaginationTruncation(t *testing.T) {
 
 	assert.Equal(t, []string{"Key1", "Key2"}, results)
 	assert.Nil(t, err)
+}
+
+func TestPaginationNilToken(t *testing.T) {
+	client := route53.New(unit.Session)
+
+	reqNum := 0
+	resps := []*route53.ListResourceRecordSetsOutput{
+		{
+			ResourceRecordSets: []*route53.ResourceRecordSet{
+				{Name: aws.String("first.example.com.")},
+			},
+			IsTruncated:          aws.Bool(true),
+			NextRecordName:       aws.String("second.example.com."),
+			NextRecordType:       aws.String("MX"),
+			NextRecordIdentifier: aws.String("second"),
+			MaxItems:             aws.String("1"),
+		},
+		{
+			ResourceRecordSets: []*route53.ResourceRecordSet{
+				{Name: aws.String("second.example.com.")},
+			},
+			IsTruncated:    aws.Bool(true),
+			NextRecordName: aws.String("third.example.com."),
+			NextRecordType: aws.String("MX"),
+			MaxItems:       aws.String("1"),
+		},
+		{
+			ResourceRecordSets: []*route53.ResourceRecordSet{
+				{Name: aws.String("third.example.com.")},
+			},
+			IsTruncated: aws.Bool(false),
+			MaxItems:    aws.String("1"),
+		},
+	}
+	client.Handlers.Send.Clear() // mock sending
+	client.Handlers.Unmarshal.Clear()
+	client.Handlers.UnmarshalMeta.Clear()
+	client.Handlers.ValidateResponse.Clear()
+
+	idents := []string{}
+	client.Handlers.Build.PushBack(func(r *request.Request) {
+		p := r.Params.(*route53.ListResourceRecordSetsInput)
+		idents = append(idents, aws.StringValue(p.StartRecordIdentifier))
+
+	})
+	client.Handlers.Unmarshal.PushBack(func(r *request.Request) {
+		r.Data = resps[reqNum]
+		reqNum++
+	})
+
+	params := &route53.ListResourceRecordSetsInput{
+		HostedZoneId: aws.String("id-zone"),
+	}
+
+	results := []string{}
+	err := client.ListResourceRecordSetsPages(params, func(p *route53.ListResourceRecordSetsOutput, last bool) bool {
+		results = append(results, *p.ResourceRecordSets[0].Name)
+		return true
+	})
 
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"", "second", ""}, idents)
+	assert.Equal(t, []string{"first.example.com.", "second.example.com.", "third.example.com."}, results)
 }
 
 // Benchmarks

+ 8 - 0
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/retryer.go

@@ -3,6 +3,7 @@ package request
 import (
 	"time"
 
+	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 )
 
@@ -15,6 +16,13 @@ type Retryer interface {
 	MaxRetries() int
 }
 
+// WithRetryer sets a config Retryer value to the given Config returning it
+// for chaining.
+func WithRetryer(cfg *aws.Config, retryer Retryer) *aws.Config {
+	cfg.Retryer = retryer
+	return cfg
+}
+
 // retryableCodes is a collection of service response codes which are retry-able
 // without any further action.
 var retryableCodes = map[string]struct{}{

+ 1 - 1
Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/version.go

@@ -5,4 +5,4 @@ package aws
 const SDKName = "aws-sdk-go"
 
 // SDKVersion is the version of this SDK
-const SDKVersion = "0.10.4"
+const SDKVersion = "1.0.0"

+ 64 - 31
Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter.go

@@ -5,6 +5,7 @@ import (
 	"reflect"
 	"time"
 
+	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/awsutil"
 	"github.com/aws/aws-sdk-go/aws/request"
@@ -47,52 +48,74 @@ func (w *Waiter) Wait() error {
 		res := method.Call([]reflect.Value{in})
 		req := res[0].Interface().(*request.Request)
 		req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("Waiter"))
-		if err := req.Send(); err != nil {
-			return err
-		}
 
+		err := req.Send()
 		for _, a := range w.Acceptors {
+			if err != nil && a.Matcher != "error" {
+				// Only matcher error is valid if there is a request error
+				continue
+			}
+
 			result := false
+			var vals []interface{}
 			switch a.Matcher {
-			case "pathAll":
-				if vals, _ := awsutil.ValuesAtPath(req.Data, a.Argument); req.Error == nil && vals != nil {
-					result = true
-					for _, val := range vals {
-						if !awsutil.DeepEqual(val, a.Expected) {
-							result = false
-							break
-						}
+			case "pathAll", "path":
+				// Require all matches to be equal for result to match
+				vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
+				result = true
+				for _, val := range vals {
+					if !awsutil.DeepEqual(val, a.Expected) {
+						result = false
+						break
 					}
 				}
 			case "pathAny":
-				if vals, _ := awsutil.ValuesAtPath(req.Data, a.Argument); req.Error == nil && vals != nil {
-					for _, val := range vals {
-						if awsutil.DeepEqual(val, a.Expected) {
-							result = true
-							break
-						}
+				// Only a single match needs to equal for the result to match
+				vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
+				for _, val := range vals {
+					if awsutil.DeepEqual(val, a.Expected) {
+						result = true
+						break
 					}
 				}
 			case "status":
 				s := a.Expected.(int)
 				result = s == req.HTTPResponse.StatusCode
+			case "error":
+				if aerr, ok := err.(awserr.Error); ok {
+					result = aerr.Code() == a.Expected.(string)
+				}
+			case "pathList":
+				// ignored matcher
+			default:
+				logf(client, "WARNING: Waiter for %s encountered unexpected matcher: %s",
+					w.Config.Operation, a.Matcher)
 			}
 
-			if result {
-				switch a.State {
-				case "success":
-					return nil // waiter completed
-				case "failure":
-					if req.Error == nil {
-						return awserr.New("ResourceNotReady",
-							fmt.Sprintf("failed waiting for successful resource state"), nil)
-					}
-					return req.Error // waiter failed
-				case "retry":
-					// do nothing, just retry
-				}
-				break
+			if !result {
+				// If there was no matching result found there is nothing more to do
+				// for this response, retry the request.
+				continue
 			}
+
+			switch a.State {
+			case "success":
+				// waiter completed
+				return nil
+			case "failure":
+				// Waiter failure state triggered
+				return awserr.New("ResourceNotReady",
+					fmt.Sprintf("failed waiting for successful resource state"), err)
+			case "retry":
+				// clear the error and retry the operation
+				err = nil
+			default:
+				logf(client, "WARNING: Waiter for %s encountered unexpected state: %s",
+					w.Config.Operation, a.State)
+			}
+		}
+		if err != nil {
+			return err
 		}
 
 		time.Sleep(time.Second * time.Duration(w.Delay))
@@ -101,3 +124,13 @@ func (w *Waiter) Wait() error {
 	return awserr.New("ResourceNotReady",
 		fmt.Sprintf("exceeded %d wait attempts", w.MaxAttempts), nil)
 }
+
+func logf(client reflect.Value, msg string, args ...interface{}) {
+	cfgVal := client.FieldByName("Config")
+	if !cfgVal.IsValid() {
+		return
+	}
+	if cfg, ok := cfgVal.Interface().(*aws.Config); ok && cfg.Logger != nil {
+		cfg.Logger.Log(fmt.Sprintf(msg, args...))
+	}
+}

+ 228 - 20
Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter_test.go

@@ -1,6 +1,9 @@
 package waiter_test
 
 import (
+	"bytes"
+	"io/ioutil"
+	"net/http"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -41,22 +44,76 @@ func (c *mockClient) MockRequest(input *MockInput) (*request.Request, *MockOutpu
 	return req, output
 }
 
-var mockAcceptors = []waiter.WaitAcceptor{
-	{
-		State:    "success",
-		Matcher:  "pathAll",
-		Argument: "States[].State",
-		Expected: "running",
-	},
-	{
-		State:    "failure",
-		Matcher:  "pathAny",
-		Argument: "States[].State",
-		Expected: "stopping",
-	},
+func TestWaiterPathAll(t *testing.T) {
+	svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
+		Region: aws.String("mock-region"),
+	})}
+	svc.Handlers.Send.Clear() // mock sending
+	svc.Handlers.Unmarshal.Clear()
+	svc.Handlers.UnmarshalMeta.Clear()
+	svc.Handlers.ValidateResponse.Clear()
+
+	reqNum := 0
+	resps := []*MockOutput{
+		{ // Request 1
+			States: []*MockState{
+				{State: aws.String("pending")},
+				{State: aws.String("pending")},
+			},
+		},
+		{ // Request 2
+			States: []*MockState{
+				{State: aws.String("running")},
+				{State: aws.String("pending")},
+			},
+		},
+		{ // Request 3
+			States: []*MockState{
+				{State: aws.String("running")},
+				{State: aws.String("running")},
+			},
+		},
+	}
+
+	numBuiltReq := 0
+	svc.Handlers.Build.PushBack(func(r *request.Request) {
+		numBuiltReq++
+	})
+	svc.Handlers.Unmarshal.PushBack(func(r *request.Request) {
+		if reqNum >= len(resps) {
+			assert.Fail(t, "too many polling requests made")
+			return
+		}
+		r.Data = resps[reqNum]
+		reqNum++
+	})
+
+	waiterCfg := waiter.Config{
+		Operation:   "Mock",
+		Delay:       0,
+		MaxAttempts: 10,
+		Acceptors: []waiter.WaitAcceptor{
+			{
+				State:    "success",
+				Matcher:  "pathAll",
+				Argument: "States[].State",
+				Expected: "running",
+			},
+		},
+	}
+	w := waiter.Waiter{
+		Client: svc,
+		Input:  &MockInput{},
+		Config: waiterCfg,
+	}
+
+	err := w.Wait()
+	assert.NoError(t, err)
+	assert.Equal(t, 3, numBuiltReq)
+	assert.Equal(t, 3, reqNum)
 }
 
-func TestWaiter(t *testing.T) {
+func TestWaiterPath(t *testing.T) {
 	svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
 		Region: aws.String("mock-region"),
 	})}
@@ -73,13 +130,13 @@ func TestWaiter(t *testing.T) {
 				{State: aws.String("pending")},
 			},
 		},
-		{ // Request 1
+		{ // Request 2
 			States: []*MockState{
 				{State: aws.String("running")},
 				{State: aws.String("pending")},
 			},
 		},
-		{ // Request 1
+		{ // Request 3
 			States: []*MockState{
 				{State: aws.String("running")},
 				{State: aws.String("running")},
@@ -104,7 +161,14 @@ func TestWaiter(t *testing.T) {
 		Operation:   "Mock",
 		Delay:       0,
 		MaxAttempts: 10,
-		Acceptors:   mockAcceptors,
+		Acceptors: []waiter.WaitAcceptor{
+			{
+				State:    "success",
+				Matcher:  "path",
+				Argument: "States[].State",
+				Expected: "running",
+			},
+		},
 	}
 	w := waiter.Waiter{
 		Client: svc,
@@ -135,13 +199,13 @@ func TestWaiterFailure(t *testing.T) {
 				{State: aws.String("pending")},
 			},
 		},
-		{ // Request 1
+		{ // Request 2
 			States: []*MockState{
 				{State: aws.String("running")},
 				{State: aws.String("pending")},
 			},
 		},
-		{ // Request 1
+		{ // Request 3
 			States: []*MockState{
 				{State: aws.String("running")},
 				{State: aws.String("stopping")},
@@ -166,7 +230,20 @@ func TestWaiterFailure(t *testing.T) {
 		Operation:   "Mock",
 		Delay:       0,
 		MaxAttempts: 10,
-		Acceptors:   mockAcceptors,
+		Acceptors: []waiter.WaitAcceptor{
+			{
+				State:    "success",
+				Matcher:  "pathAll",
+				Argument: "States[].State",
+				Expected: "running",
+			},
+			{
+				State:    "failure",
+				Matcher:  "pathAny",
+				Argument: "States[].State",
+				Expected: "stopping",
+			},
+		},
 	}
 	w := waiter.Waiter{
 		Client: svc,
@@ -181,3 +258,134 @@ func TestWaiterFailure(t *testing.T) {
 	assert.Equal(t, 3, numBuiltReq)
 	assert.Equal(t, 3, reqNum)
 }
+
+func TestWaiterError(t *testing.T) {
+	svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
+		Region: aws.String("mock-region"),
+	})}
+	svc.Handlers.Send.Clear() // mock sending
+	svc.Handlers.Unmarshal.Clear()
+	svc.Handlers.UnmarshalMeta.Clear()
+	svc.Handlers.ValidateResponse.Clear()
+
+	reqNum := 0
+	resps := []*MockOutput{
+		{ // Request 1
+			States: []*MockState{
+				{State: aws.String("pending")},
+				{State: aws.String("pending")},
+			},
+		},
+		{ // Request 2, error case
+		},
+		{ // Request 3
+			States: []*MockState{
+				{State: aws.String("running")},
+				{State: aws.String("running")},
+			},
+		},
+	}
+
+	numBuiltReq := 0
+	svc.Handlers.Build.PushBack(func(r *request.Request) {
+		numBuiltReq++
+	})
+	svc.Handlers.Send.PushBack(func(r *request.Request) {
+		if reqNum == 1 {
+			r.Error = awserr.New("MockException", "mock exception message", nil)
+			r.HTTPResponse = &http.Response{
+				StatusCode: 400,
+				Status:     http.StatusText(400),
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte{})),
+			}
+			reqNum++
+		}
+	})
+	svc.Handlers.Unmarshal.PushBack(func(r *request.Request) {
+		if reqNum >= len(resps) {
+			assert.Fail(t, "too many polling requests made")
+			return
+		}
+		r.Data = resps[reqNum]
+		reqNum++
+	})
+
+	waiterCfg := waiter.Config{
+		Operation:   "Mock",
+		Delay:       0,
+		MaxAttempts: 10,
+		Acceptors: []waiter.WaitAcceptor{
+			{
+				State:    "success",
+				Matcher:  "pathAll",
+				Argument: "States[].State",
+				Expected: "running",
+			},
+			{
+				State:    "retry",
+				Matcher:  "error",
+				Argument: "",
+				Expected: "MockException",
+			},
+		},
+	}
+	w := waiter.Waiter{
+		Client: svc,
+		Input:  &MockInput{},
+		Config: waiterCfg,
+	}
+
+	err := w.Wait()
+	assert.NoError(t, err)
+	assert.Equal(t, 3, numBuiltReq)
+	assert.Equal(t, 3, reqNum)
+}
+
+func TestWaiterStatus(t *testing.T) {
+	svc := &mockClient{Client: awstesting.NewClient(&aws.Config{
+		Region: aws.String("mock-region"),
+	})}
+	svc.Handlers.Send.Clear() // mock sending
+	svc.Handlers.Unmarshal.Clear()
+	svc.Handlers.UnmarshalMeta.Clear()
+	svc.Handlers.ValidateResponse.Clear()
+
+	reqNum := 0
+	svc.Handlers.Build.PushBack(func(r *request.Request) {
+		reqNum++
+	})
+	svc.Handlers.Send.PushBack(func(r *request.Request) {
+		code := 200
+		if reqNum == 3 {
+			code = 404
+		}
+		r.HTTPResponse = &http.Response{
+			StatusCode: code,
+			Status:     http.StatusText(code),
+			Body:       ioutil.NopCloser(bytes.NewReader([]byte{})),
+		}
+	})
+
+	waiterCfg := waiter.Config{
+		Operation:   "Mock",
+		Delay:       0,
+		MaxAttempts: 10,
+		Acceptors: []waiter.WaitAcceptor{
+			{
+				State:    "success",
+				Matcher:  "status",
+				Argument: "",
+				Expected: 404,
+			},
+		},
+	}
+	w := waiter.Waiter{
+		Client: svc,
+		Input:  &MockInput{},
+		Config: waiterCfg,
+	}
+
+	err := w.Wait()
+	assert.NoError(t, err)
+	assert.Equal(t, 3, reqNum)
+}

+ 16 - 1
build.go

@@ -76,6 +76,14 @@ func main() {
 			grunt("release")
 			createLinuxPackages()
 
+		case "pkg-rpm":
+			grunt("release")
+			createRpmPackages()
+
+		case "pkg-deb":
+			grunt("release")
+			createDebPackages()
+
 		case "latest":
 			makeLatestDistCopies()
 
@@ -147,7 +155,7 @@ type linuxPackageOptions struct {
 	depends []string
 }
 
-func createLinuxPackages() {
+func createDebPackages() {
 	createPackage(linuxPackageOptions{
 		packageType:            "deb",
 		homeDir:                "/usr/share/grafana",
@@ -167,7 +175,9 @@ func createLinuxPackages() {
 
 		depends: []string{"adduser", "libfontconfig"},
 	})
+}
 
+func createRpmPackages() {
 	createPackage(linuxPackageOptions{
 		packageType:            "rpm",
 		homeDir:                "/usr/share/grafana",
@@ -189,6 +199,11 @@ func createLinuxPackages() {
 	})
 }
 
+func createLinuxPackages() {
+	createDebPackages()
+	createRpmPackages()
+}
+
 func createPackage(options linuxPackageOptions) {
 	packageRoot, _ := ioutil.TempDir("", "grafana-linux-pack")
 

+ 25 - 0
conf/defaults.ini

@@ -15,6 +15,10 @@ data = data
 # Directory where grafana can store logs
 #
 logs = data/log
+#
+# Directory where grafana will automatically scan and look for plugins
+#
+plugins = data/plugins
 
 #################################### Server ####################################
 [server]
@@ -125,6 +129,12 @@ disable_gravatar = false
 # data source proxy whitelist (ip_or_domain:port seperated by spaces)
 data_source_proxy_whitelist =
 
+[snapshots]
+# snapshot sharing options
+external_enabled = true
+external_snapshot_url = https://snapshots-origin.raintank.io
+external_snapshot_name = Publish to snapshot.raintank.io
+
 #################################### Users ####################################
 [users]
 # disable user signup / registration
@@ -142,6 +152,9 @@ auto_assign_org_role = Viewer
 # Require email validation before sign up completes
 verify_email_enabled = false
 
+# Background text for the user field on the login page
+login_hint = email or username
+
 #################################### Anonymous Auth ##########################
 [auth.anonymous]
 # enable anonymous access
@@ -245,6 +258,18 @@ daily_rotate = true
 # Expired days of log file(delete after max days), default is 7
 max_days = 7
 
+[log.syslog]
+level =
+# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
+network =
+address =
+
+# Syslog facility. user, daemon and local0 through local7 are valid.
+facility =
+
+# Syslog tag. By default, the process' argv[0] is used.
+tag =
+
 #################################### AMPQ Event Publisher ##########################
 [event_publisher]
 enabled = false

+ 14 - 0
conf/sample.ini

@@ -15,7 +15,12 @@
 # Directory where grafana can store logs
 #
 ;logs = /var/log/grafana
+#
+# Directory where grafana will automatically scan and look for plugins
+#
+;plugins = /var/lib/grafana/plugins
 
+#
 #################################### Server ####################################
 [server]
 # Protocol (http or https)
@@ -120,6 +125,12 @@
 # data source proxy whitelist (ip_or_domain:port seperated by spaces)
 ;data_source_proxy_whitelist =
 
+[snapshots]
+# snapshot sharing options
+;external_enabled = true
+;external_snapshot_url = https://snapshots-origin.raintank.io
+;external_snapshot_name = Publish to snapshot.raintank.io
+
 #################################### Users ####################################
 [users]
 # disable user signup / registration
@@ -134,6 +145,9 @@
 # Default role new users will be automatically assigned (if disabled above is set to true)
 ;auto_assign_org_role = Viewer
 
+# Background text for the user field on the login page
+;login_hint = email or username
+
 #################################### Anonymous Auth ##########################
 [auth.anonymous]
 # enable anonymous access

+ 1 - 1
docs/VERSION

@@ -1 +1 @@
-2.5.0
+2.6.0

+ 6 - 4
docs/mkdocs.yml

@@ -45,6 +45,7 @@ pages:
 
 - ['guides/basic_concepts.md', 'User Guides', 'Basic Concepts']
 - ['guides/gettingstarted.md', 'User Guides', 'Getting Started']
+- ['guides/whats-new-in-v2-6.md', 'User Guides', "What's New in Grafana v2.6"]
 - ['guides/whats-new-in-v2-5.md', 'User Guides', "What's New in Grafana v2.5"]
 - ['guides/whats-new-in-v2-1.md', 'User Guides', "What's New in Grafana v2.1"]
 - ['guides/whats-new-in-v2.md', 'User Guides', "What's New in Grafana v2.0"]
@@ -52,13 +53,14 @@ pages:
 
 - ['reference/graph.md', 'Reference', 'Graph Panel']
 - ['reference/singlestat.md', 'Reference', 'Singlestat Panel']
+- ['reference/table_panel.md', 'Reference', 'Table Panel']
 - ['reference/dashlist.md', 'Reference', 'Dashboard List Panel']
 - ['reference/sharing.md', 'Reference', 'Sharing']
 - ['reference/annotations.md', 'Reference', 'Annotations']
-- ['reference/timerange.md', 'Reference', 'Time Range Controls']
-- ['reference/search.md', 'Reference', 'Dashboard Search']
-- ['reference/templating.md', 'Reference', 'Templated Dashboards']
-- ['reference/scripting.md', 'Reference', 'Scripted Dashboards']
+- ['reference/timerange.md', 'Reference', 'Time Range']
+- ['reference/search.md', 'Reference', 'Search']
+- ['reference/templating.md', 'Reference', 'Templating']
+- ['reference/scripting.md', 'Reference', 'Scripting']
 - ['reference/playlist.md', 'Reference', 'Playlist']
 - ['reference/export_import.md', 'Reference', 'Import & Export']
 - ['reference/admin.md', 'Reference', 'Administration']

+ 29 - 0
docs/sources/datasources/elasticsearch.md

@@ -53,6 +53,35 @@ a time pattern for the index name or a wildcard.
 The Elasticsearch query editor allows you to select multiple metrics and group by multiple terms or filters. Use the plus and minus icons to the right to add / remove
 metrics or group bys. Some metrics and group by have options, click the option text to expand the the row to view and edit metric or group by options.
 
+## Pipeline metrics
+
+If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline metric aggregations like
+**Moving Average** and **Derivative**. Elasticsearch pipeline metrics require another metric to be based on. Use the eye icon next to the metric
+to hide metrics from appearing in the graph. This is useful for metrics you only have in the query to be used
+in a pipeline metric.
+
+![](/img/elasticsearch/pipeline_metrics_editor.png)
+
+## Templating
+
+The Elasticsearch datasource supports two types of queries you can use to fill template variables with values.
+
+### Possible values for a field
+
+```json
+{"find": "terms", "field": "@hostname"}
+```
+
+### Fields filtered by type
+```json
+{"find": "fields", "type": "string"}
+```
+
+### Multi format / All format
+Use lucene format.
+
+
+
 ## Annotations
 TODO
 

+ 38 - 18
docs/sources/datasources/influxdb.md

@@ -38,29 +38,47 @@ Password | Database user's password
  > Direct access is still supported because in some cases it may be useful to access a Data Source directly depending on the use case and topology of Grafana, the user, and the Data Source.
 
 
-## InfluxDB 0.9.x
+## Query Editor
 
-![](/img/influxdb/InfluxDB_09_editor.png)
+![](/img/influxdb/editor_v3.png)
 
 You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the
 panel title, then edit. The editor allows you to select metrics and tags.
 
-### Editor tag filters
+### Filter data (WHERE)
 To add a tag filter click the plus icon to the right of the `WHERE` condition. You can remove tag filters by clicking on
 the tag key and select `--remove tag filter--`.
 
-### Regex matching
+**Regex matching**
+
 You can type in regex patterns for metric names or tag filter values, be sure to wrap the regex pattern in forward slashes (`/`). Grafana
 will automatically adjust the filter tag condition to use the InfluxDB regex match condition operator (`=~`).
 
-### Editor group by
-To group by a tag click the plus icon after the `GROUP BY ($interval)` text. Pick a tag from the dropdown that appears.
-You can remove the group by by clicking on the tag and then select `--remove group by--` from the dropdown.
+### Field & Aggregation functions
+In the `SELECT` row you can specify what fields and functions you want to use. If you have a
+group by time you need an aggregation function. Some functions like derivative require an aggregation function.
+
+The editor tries simplify and unify this part of the query. For example:
+![](/img/influxdb/select_editor.png)
+
+The above will generate the following InfluxDB `SELECT` clause:
+
+```sql
+SELECT derivative(mean("value"), 10s) /10 AS "REQ/s" FROM ....
+```
+
+#### Select multiple fields
+Use the plus button and select Field > field to add another SELECT clause. You can also
+specify an asterix `*` to select all fields.
+
+### Group By
+To group by a tag click the plus icon at the end of the GROUP BY row. Pick a tag from the dropdown that appears.
+You can remove the group by by clicking on the `tag` and then click on the x icon.
 
-### Editor RAW Query
-You can switch to raw query mode by pressing the pen icon.
+### Text Editor Mode (RAW)
+You can switch to raw query mode by clicking hamburger icon and then `Switch editor mode`.
 
-> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter` clause and ends with `order by asc`.
+> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter`
 > Also please always have a group by time and an aggregation function, otherwise InfluxDB can easily return hundreds of thousands
 > of data points that will hang the browser.
 
@@ -72,7 +90,15 @@ You can switch to raw query mode by pressing the pen icon.
 - $tag_hostname = replaced with the value of the hostname tag
 - You can also use [[tag_hostname]] pattern replacement syntax
 
-### Templating
+### Table query / raw data
+
+![](/img/influxdb/raw_data.png)
+
+You can remove the group by time by clicking on the `time` part and then the `x` icon. You can
+change the option `Format As` to `Table` if you want to show raw data in the `Table` panel.
+
+
+## Templating
 You can create a template variable in Grafana and have that variable filled with values from any InfluxDB metric exploration query.
 You can then use this variable in your InfluxDB metric queries.
 
@@ -93,7 +119,7 @@ SHOW TAG VALUES WITH KEY = "hostname"  WHERE region =~ /$region/
 
 ![](/img/influxdb/templating_simple_ex1.png)
 
-### Annotations
+## Annotations
 Annotations allows you to overlay rich event information on top of graphs.
 
 An example query:
@@ -102,10 +128,4 @@ An example query:
 SELECT title, description from events WHERE $timeFilter order asc
 ```
 
-### InfluxDB 0.8.x
-
-![](/img/v1/influxdb_editor.png)
-
-
-
 

+ 123 - 0
docs/sources/guides/whats-new-in-v2-6.md

@@ -0,0 +1,123 @@
+---
+page_title: What's New in Grafana v2.6
+page_description: What's new in Grafana v2.6
+page_keywords: grafana, new, changes, features, documentation, table
+---
+
+# What's new in Grafana v2.6
+
+## Release highlights
+The release includes a new Table panel, a new InfluxDB query editor, support for Elasticsearch Pipeline Metrics and
+support for multiple Cloudwatch credentials.
+
+## Table Panel
+<img src="/img/v2/table-panel.png">
+
+The new table panel is very flexible, supporting both multiple modes for time series as well as for
+table, annotation and raw JSON data. It also provides date formating and value formating and coloring options.
+
+### Time series to rows
+
+In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column.
+Where `Metric` is the name of the time series.
+
+<img src="/img/v2.6/table_ts_to_rows.png">
+
+### Table Transform
+Above you see the options tab for the **Table Panel**. The most important option is the `To Table Transform`.
+This option controls how the result of the metric/data query is turned into a table.
+
+### Column Styles
+The column styles allow you control how dates and numbers are formatted.
+
+### Time series to columns
+This transform allows you to take multiple time series and group them by time. Which will result in a `Time` column
+and a column for each time series.
+
+<img src="/img/v2.6/table_ts_to_columns.png">
+
+In the screenshot above you can see how the same time series query as in the previous example can be transformed into
+a different table by changing the `To Table Transform` to  `Time series to columns`.
+
+### Time series to aggregations
+This transform works very similar to the legend values in the Graph panel. Each series gets its own row. In the Options
+tab you can select which aggregations you want using the plus button the Columns section.
+
+<img src="/img/v2.6/table_ts_to_aggregations.png">
+
+You have to think about how accurate the aggregations will be. It depends on what aggregation is used in the time series query,
+how many data points are fetched, etc. The time series aggregations are calculated by Grafana after aggregation is performed
+by the time series database.
+
+### Raw logs queries
+
+If you want to show documents from Elasticsearch pick `Raw Document` as the first metric.
+
+<img src="/img/v2.6/elastic_raw_doc.png">
+
+This in combination with the `JSON Data` table transform will allow you to pick which fields in the document
+you want to show in the table.
+
+<img src="/img/v2.6/table_json_data.png">
+
+### Elasticsearch aggregations
+
+You can also make Elasticsearch aggregation queries without a `Date Histogram`. This allows you to
+use Elasticsearch metric aggregations to get accurate aggregations for the selected time range.
+
+<img src="/img/v2.6/elastic_aggregations.png">
+
+### Annotations
+
+The table can also show any annotations you have enabled in the dashboard.
+
+<img src="/img/v2.6/table_annotations.png">
+
+## The New InfluxDB Editor
+The new InfluxDB editor is a lot more flexible and powerful. It supports nested functions, like `derivative`.
+It also uses the same technique as the Graphite query editor in that it presents nested functions as chain of function
+transformations. It tries to simplify and unify the complicated nature of InfluxDB's query language.
+
+<img src="/img/v2.6/influxdb_editor_v3.gif">
+
+In the `SELECT` row you can specify what fields and functions you want to use. If you have a
+group by time you need an aggregation function. Some functions like derivative require an aggregation function.
+
+The editor tries simplify and unify this part of the query. For example:
+![](/img/influxdb/select_editor.png)
+
+The above will generate the following InfluxDB `SELECT` clause:
+
+```sql
+SELECT derivative(mean("value"), 10s) /10 AS "REQ/s" FROM ....
+```
+
+### Select multiple fields
+Use the plus button and select Field > field to add another SELECT clause. You can also
+specify an asterix `*` to select all fields.
+
+### Group By
+To group by a tag click the plus icon at the end of the GROUP BY row. Pick a tag from the dropdown that appears.
+You can remove the group by by clicking on the `tag` and then click on the x icon.
+
+The new editor also allows you to remove group by time and select `raw` table data. Which is very useful
+in combination with the new Table panel to show raw log data stored in InfluxDB.
+
+<img src="/img/v2.6/table_influxdb_logs.png">
+
+## Pipeline metrics
+
+If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline metric aggregations like
+**Moving Average** and **Derivative**. Elasticsearch pipeline metrics require another metric to be based on. Use the eye icon next to the metric
+to hide metrics from appearing in the graph.
+
+![](/img/elasticsearch/pipeline_metrics_editor.png)
+
+## Changelog
+For a detailed list and link to github issues for everything included in the 2.6 release please
+view the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file.
+
+- - -
+
+<a href="http://grafana.org/download">Download Grafana 2.6 now</a>
+

+ 3 - 3
docs/sources/installation/debian.md

@@ -10,13 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
 
 Description | Download
 ------------ | -------------
-.deb for Debian-based Linux | [grafana_2.5.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.5.0_amd64.deb)
+.deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb)
 
 ## Install
 
-    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.5.0_amd64.deb
+    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb
     $ sudo apt-get install -y adduser libfontconfig
-    $ sudo dpkg -i grafana_2.5.0_amd64.deb
+    $ sudo dpkg -i grafana_2.6.0_amd64.deb
 
 ## APT Repository
 

+ 4 - 4
docs/sources/installation/rpm.md

@@ -10,24 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
 
 Description | Download
 ------------ | -------------
-.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.5.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.5.0-1.x86_64.rpm)
+.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm)
 
 ## Install from package file
 
 You can install Grafana using Yum directly.
 
-    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.5.0-1.x86_64.rpm
+    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm
 
 Or install manually using `rpm`.
 
 #### On CentOS / Fedora / Redhat:
 
     $ sudo yum install initscripts fontconfig
-    $ sudo rpm -Uvh grafana-2.5.0-1.x86_64.rpm
+    $ sudo rpm -Uvh grafana-2.6.0-1.x86_64.rpm
 
 #### On OpenSuse:
 
-    $ sudo rpm -i --nodeps grafana-2.5.0-1.x86_64.rpm
+    $ sudo rpm -i --nodeps grafana-2.6.0-1.x86_64.rpm
 
 ## Install via YUM Repository
 

+ 86 - 0
docs/sources/reference/table_panel.md

@@ -0,0 +1,86 @@
+----
+page_title: Table Panel
+page_description: Table Panel Reference
+page_keywords: grafana, table, panel, documentation
+---
+
+# Table Panel
+
+<img src="/img/v2/table-panel.png">
+
+The new table panel is very flexible, supporting both multiple modes for time series as well as for
+table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options.
+
+To view table panels in action and test different configurations with sample data, check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase).
+
+## Options overview
+
+The table panel has many ways to manipulate your data for optimal presentation.
+
+<img class="no-shadow" src="/img/v2/table-config.png">
+
+1. `Data`: Control how your query is transformed into a table.
+2. `Table Display`: Table display options.
+3. `Column Styles`: Column value formatting and display options.
+
+## Data to Table
+
+<img class="no-shadow" src="/img/v2/table-data-options.png">
+
+The data section contains the **To Table Transform (1)**. This is the primary option for how your data/metric
+query should be transformed into a table format.  The **Columns (2)** option allows you to select what columns
+you want in the table. Only applicable for some transforms.
+
+### Time series to rows
+
+<img src="/img/v2/table_ts_to_rows.png">
+
+In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series.
+
+### Time series to columns
+
+![](/img/v2/table_ts_to_columns.png)
+
+This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series.
+
+### Time series aggregations
+
+![](/img/v2/table_ts_to_aggregations.png)
+This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added.
+
+### Annotations
+![](/img/v2/table_annotations.png)
+
+If you have annotations enabled in the dashboard you can have the table show them. If you configure this
+mode then any queries you have in the metrics tab will be ignored.
+
+### JSON Data
+![](/img/v2/table_json_data.png)
+
+If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this
+transform mode and pick the columns using the **Columns** section.
+
+![](/img/v2/elastic_raw_doc.png)
+
+## Table Display
+
+<img class="no-shadow" src="/img/v2/table-display.png">
+
+1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages.
+2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows.
+3. `Font Size`: The `font size` field allows you to increase or decrease the size for the panel, relative to the default font size.
+
+
+## Column Styles
+
+The column styles allow you control how dates and numbers are formatted.
+
+<img class="no-shadow" src="/img/v2/Column-Options.png">
+
+1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
+2. `Type`: The three supported types of types are `Number`, `String` and `Date`.
+3. `Format`: Specify date format. Only available when `Type` is set to `Date`.
+4. `Coloring` and `Thresholds`: Specify color mode and thresholds limits.
+5. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.
+6.  `Add column style rule`: Add new column rule.
+

+ 1 - 0
docs/sources/versions.html_fragment

@@ -1,3 +1,4 @@
+<li><a class='version' href='/v2.6'>Version v2.6</a></li>
 <li><a class='version' href='/v2.5'>Version v2.5</a></li>
 <li><a class='version' href='/v2.1'>Version v2.1</a></li>
 <li><a class='version' href='/v2.0'>Version v2.0</a></li>

+ 1 - 1
package.json

@@ -4,7 +4,7 @@
     "company": "Coding Instinct AB"
   },
   "name": "grafana",
-  "version": "2.6.0-beta1",
+  "version": "3.0.0-pre1",
   "repository": {
     "type": "git",
     "url": "http://github.com/torkelo/grafana.git"

+ 2 - 0
packaging/deb/default/grafana-server

@@ -15,3 +15,5 @@ CONF_DIR=/etc/grafana
 CONF_FILE=/etc/grafana/grafana.ini
 
 RESTART_ON_UPGRADE=false
+
+PLUGINS_DIR=/var/lib/grafana/plugins

+ 3 - 1
packaging/deb/init.d/grafana-server

@@ -30,12 +30,14 @@ GRAFANA_HOME=/usr/share/grafana
 CONF_DIR=/etc/grafana
 WORK_DIR=$GRAFANA_HOME
 DATA_DIR=/var/lib/grafana
+PLUGINS_DIR=/var/lib/grafana/plugins
 LOG_DIR=/var/log/grafana
 CONF_FILE=$CONF_DIR/grafana.ini
 MAX_OPEN_FILES=10000
 PID_FILE=/var/run/$NAME.pid
 DAEMON=/usr/sbin/$NAME
 
+
 umask 0027
 
 if [ `id -u` -ne 0 ]; then
@@ -59,7 +61,7 @@ if [ -f "$DEFAULT" ]; then
 	. "$DEFAULT"
 fi
 
-DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}"
+DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
 
 case "$1" in
   start)

+ 2 - 1
packaging/deb/systemd/grafana-server.service

@@ -14,7 +14,8 @@ ExecStart=/usr/sbin/grafana-server                                \
                             --config=${CONF_FILE}                 \
                             --pidfile=${PID_FILE}                 \
                             cfg:default.paths.logs=${LOG_DIR}     \
-                            cfg:default.paths.data=${DATA_DIR}
+                            cfg:default.paths.data=${DATA_DIR}    \
+                            cfg:default.paths.plugins=${PLUGINS_DIR}
 LimitNOFILE=10000
 TimeoutStopSec=20
 UMask=0027

+ 1 - 1
packaging/publish/publish.sh

@@ -1,6 +1,6 @@
 #! /usr/bin/env bash
 
-version=2.5.0
+version=2.6.0
 
 wget https://grafanarel.s3.amazonaws.com/builds/grafana_${version}_amd64.deb
 

+ 2 - 1
packaging/rpm/init.d/grafana-server

@@ -29,6 +29,7 @@ GRAFANA_HOME=/usr/share/grafana
 CONF_DIR=/etc/grafana
 WORK_DIR=$GRAFANA_HOME
 DATA_DIR=/var/lib/grafana
+PLUGINS_DIR=/var/lib/grafana/plugins
 LOG_DIR=/var/log/grafana
 CONF_FILE=$CONF_DIR/grafana.ini
 MAX_OPEN_FILES=10000
@@ -63,7 +64,7 @@ fi
 # overwrite settings from default file
 [ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
 
-DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}"
+DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
 
 function isRunning() {
   status -p $PID_FILE $NAME > /dev/null 2>&1

+ 2 - 0
packaging/rpm/sysconfig/grafana-server

@@ -15,3 +15,5 @@ CONF_DIR=/etc/grafana
 CONF_FILE=/etc/grafana/grafana.ini
 
 RESTART_ON_UPGRADE=false
+
+PLUGINS_DIR=/var/lib/grafana/plugins

+ 2 - 1
packaging/rpm/systemd/grafana-server.service

@@ -14,7 +14,8 @@ ExecStart=/usr/sbin/grafana-server                                \
                             --config=${CONF_FILE}                 \
                             --pidfile=${PID_FILE}                 \
                             cfg:default.paths.logs=${LOG_DIR}     \
-                            cfg:default.paths.data=${DATA_DIR}
+                            cfg:default.paths.data=${DATA_DIR}    \
+                            cfg:default.paths.plugins=${PLUGINS_DIR}
 LimitNOFILE=10000
 TimeoutStopSec=20
 

+ 8 - 4
pkg/api/api.go

@@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) {
 	reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
-	regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
+	reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
 	quota := middleware.Quota
 	bind := binding.Bind
 
@@ -41,6 +41,9 @@ func Register(r *macaron.Macaron) {
 	r.Get("/admin/orgs", reqGrafanaAdmin, Index)
 	r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
 
+	r.Get("/plugins", reqSignedIn, Index)
+	r.Get("/plugins/edit/*", reqSignedIn, Index)
+
 	r.Get("/dashboard/*", reqSignedIn, Index)
 	r.Get("/dashboard-solo/*", reqSignedIn, Index)
 
@@ -65,6 +68,7 @@ func Register(r *macaron.Macaron) {
 	r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
 	r.Get("/dashboard/snapshot/*", Index)
 
+	r.Get("/api/snapshot/shared-options/", GetSharingOptions)
 	r.Get("/api/snapshots/:key", GetDashboardSnapshot)
 	r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
 
@@ -113,7 +117,7 @@ func Register(r *macaron.Macaron) {
 			r.Get("/invites", wrap(GetPendingOrgInvites))
 			r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
 			r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
-		}, regOrgAdmin)
+		}, reqOrgAdmin)
 
 		// create new org
 		r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
@@ -140,7 +144,7 @@ func Register(r *macaron.Macaron) {
 			r.Get("/", wrap(GetApiKeys))
 			r.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
 			r.Delete("/:id", wrap(DeleteApiKey))
-		}, regOrgAdmin)
+		}, reqOrgAdmin)
 
 		// Data sources
 		r.Group("/datasources", func() {
@@ -150,7 +154,7 @@ func Register(r *macaron.Macaron) {
 			r.Delete("/:id", DeleteDataSource)
 			r.Get("/:id", wrap(GetDataSourceById))
 			r.Get("/plugins", GetDataSourcePlugins)
-		}, regOrgAdmin)
+		}, reqOrgAdmin)
 
 		r.Get("/frontend/settings/", GetFrontendSettings)
 		r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)

+ 120 - 9
pkg/api/cloudwatch/cloudwatch.go

@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/awsutil"
 	"github.com/aws/aws-sdk-go/aws/credentials"
 	"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
 	"github.com/aws/aws-sdk-go/aws/ec2metadata"
@@ -30,13 +31,15 @@ type cwRequest struct {
 
 func init() {
 	actionHandlers = map[string]actionHandler{
-		"GetMetricStatistics": handleGetMetricStatistics,
-		"ListMetrics":         handleListMetrics,
-		"DescribeInstances":   handleDescribeInstances,
-		"__GetRegions":        handleGetRegions,
-		"__GetNamespaces":     handleGetNamespaces,
-		"__GetMetrics":        handleGetMetrics,
-		"__GetDimensions":     handleGetDimensions,
+		"GetMetricStatistics":     handleGetMetricStatistics,
+		"ListMetrics":             handleListMetrics,
+		"DescribeAlarmsForMetric": handleDescribeAlarmsForMetric,
+		"DescribeAlarmHistory":    handleDescribeAlarmHistory,
+		"DescribeInstances":       handleDescribeInstances,
+		"__GetRegions":            handleGetRegions,
+		"__GetNamespaces":         handleGetNamespaces,
+		"__GetMetrics":            handleGetMetrics,
+		"__GetDimensions":         handleGetDimensions,
 	}
 }
 
@@ -119,7 +122,107 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
 		Dimensions: reqParam.Parameters.Dimensions,
 	}
 
-	resp, err := svc.ListMetrics(params)
+	var resp cloudwatch.ListMetricsOutput
+	err := svc.ListMetricsPages(params,
+		func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
+			metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
+			for _, metric := range metrics {
+				resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
+			}
+			return !lastPage
+		})
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
+
+	c.JSON(200, resp)
+}
+
+func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
+	sess := session.New()
+	creds := credentials.NewChainCredentials(
+		[]credentials.Provider{
+			&credentials.EnvProvider{},
+			&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
+			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
+		})
+
+	cfg := &aws.Config{
+		Region:      aws.String(req.Region),
+		Credentials: creds,
+	}
+
+	svc := cloudwatch.New(session.New(cfg), cfg)
+
+	reqParam := &struct {
+		Parameters struct {
+			Namespace  string                  `json:"namespace"`
+			MetricName string                  `json:"metricName"`
+			Dimensions []*cloudwatch.Dimension `json:"dimensions"`
+			Statistic  string                  `json:"statistic"`
+			Period     int64                   `json:"period"`
+		} `json:"parameters"`
+	}{}
+	json.Unmarshal(req.Body, reqParam)
+
+	params := &cloudwatch.DescribeAlarmsForMetricInput{
+		Namespace:  aws.String(reqParam.Parameters.Namespace),
+		MetricName: aws.String(reqParam.Parameters.MetricName),
+		Period:     aws.Int64(reqParam.Parameters.Period),
+	}
+	if len(reqParam.Parameters.Dimensions) != 0 {
+		params.Dimensions = reqParam.Parameters.Dimensions
+	}
+	if reqParam.Parameters.Statistic != "" {
+		params.Statistic = aws.String(reqParam.Parameters.Statistic)
+	}
+
+	resp, err := svc.DescribeAlarmsForMetric(params)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
+
+	c.JSON(200, resp)
+}
+
+func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
+	sess := session.New()
+	creds := credentials.NewChainCredentials(
+		[]credentials.Provider{
+			&credentials.EnvProvider{},
+			&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
+			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
+		})
+
+	cfg := &aws.Config{
+		Region:      aws.String(req.Region),
+		Credentials: creds,
+	}
+
+	svc := cloudwatch.New(session.New(cfg), cfg)
+
+	reqParam := &struct {
+		Parameters struct {
+			AlarmName       string `json:"alarmName"`
+			HistoryItemType string `json:"historyItemType"`
+			StartDate       int64  `json:"startDate"`
+			EndDate         int64  `json:"endDate"`
+		} `json:"parameters"`
+	}{}
+	json.Unmarshal(req.Body, reqParam)
+
+	params := &cloudwatch.DescribeAlarmHistoryInput{
+		AlarmName: aws.String(reqParam.Parameters.AlarmName),
+		StartDate: aws.Time(time.Unix(reqParam.Parameters.StartDate, 0)),
+		EndDate:   aws.Time(time.Unix(reqParam.Parameters.EndDate, 0)),
+	}
+	if reqParam.Parameters.HistoryItemType != "" {
+		params.HistoryItemType = aws.String(reqParam.Parameters.HistoryItemType)
+	}
+
+	resp, err := svc.DescribeAlarmHistory(params)
 	if err != nil {
 		c.JsonApiErr(500, "Unable to call AWS API", err)
 		return
@@ -160,7 +263,15 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
 		params.InstanceIds = reqParam.Parameters.InstanceIds
 	}
 
-	resp, err := svc.DescribeInstances(params)
+	var resp ec2.DescribeInstancesOutput
+	err := svc.DescribeInstancesPages(params,
+		func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
+			reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
+			for _, reservation := range reservations {
+				resp.Reservations = append(resp.Reservations, reservation.(*ec2.Reservation))
+			}
+			return !lastPage
+		})
 	if err != nil {
 		c.JsonApiErr(500, "Unable to call AWS API", err)
 		return

+ 43 - 22
pkg/api/cloudwatch/metrics.go

@@ -15,31 +15,47 @@ func init() {
 	metricsMap = map[string][]string{
 		"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
 		"AWS/Billing":     {"EstimatedCharges"},
-		"AWS/EC2":         {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
-		"AWS/ECS":         {"CPUUtilization", "MemoryUtilization"},
 		"AWS/CloudFront":  {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
 		"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
 		"AWS/DynamoDB":    {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedItemCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
+		"AWS/ECS":         {"CPUUtilization", "MemoryUtilization"},
 		"AWS/ElastiCache": {
-			"CPUUtilization", "SwapUsage", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut",
+			"CPUUtilization", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut", "SwapUsage",
 			"BytesUsedForCacheItems", "BytesReadIntoMemcached", "BytesWrittenOutFromMemcached", "CasBadval", "CasHits", "CasMisses", "CmdFlush", "CmdGet", "CmdSet", "CurrConnections", "CurrItems", "DecrHits", "DecrMisses", "DeleteHits", "DeleteMisses", "Evictions", "GetHits", "GetMisses", "IncrHits", "IncrMisses", "Reclaimed",
-			"CurrConnections", "Evictions", "Reclaimed", "NewConnections", "BytesUsedForCache", "CacheHits", "CacheMisses", "ReplicationLag", "GetTypeCmds", "SetTypeCmds", "KeyBasedCmds", "StringBasedCmds", "HashBasedCmds", "ListBasedCmds", "SetBasedCmds", "SortedSetBasedCmds", "CurrItems",
+			"BytesUsedForHash", "CmdConfigGet", "CmdConfigSet", "CmdTouch", "CurrConfig", "EvictedUnfetched", "ExpiredUnfetched", "SlabsMoved", "TouchHits", "TouchMisses",
+			"NewConnections", "NewItems", "UnusedMemory",
+			"BytesUsedForCache", "CacheHits", "CacheMisses", "CurrConnections", "Evictions", "HyperLogLogBasedCmds", "NewConnections", "Reclaimed", "ReplicationBytes", "ReplicationLag", "SaveInProgress",
+			"CurrItems", "GetTypeCmds", "HashBasedCmds", "KeyBasedCmds", "ListBasedCmds", "SetBasedCmds", "SetTypeCmds", "SortedSetBasedCmds", "StringBasedCmds",
 		},
-		"AWS/EBS":              {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"},
-		"AWS/ELB":              {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"},
-		"AWS/ElasticMapReduce": {"CoreNodesPending", "CoreNodesRunning", "HBaseBackupFailed", "HBaseMostRecentBackupDuration", "HBaseTimeSinceLastSuccessfulBackup", "HDFSBytesRead", "HDFSBytesWritten", "HDFSUtilization", "IsIdle", "JobsFailed", "JobsRunning", "LiveDataNodes", "LiveTaskTrackers", "MapSlotsOpen", "MissingBlocks", "ReduceSlotsOpen", "RemainingMapTasks", "RemainingMapTasksPerSlot", "RemainingReduceTasks", "RunningMapTasks", "RunningReduceTasks", "S3BytesRead", "S3BytesWritten", "TaskNodesPending", "TaskNodesRunning", "TotalLoad"},
-		"AWS/Kinesis":          {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"},
-		"AWS/ML":               {"PredictCount", "PredictFailureCount"},
-		"AWS/OpsWorks":         {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
-		"AWS/Redshift":         {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
-		"AWS/RDS":              {"BinLogDiskUsage", "CPUUtilization", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"},
-		"AWS/Route53":          {"HealthCheckStatus", "HealthCheckPercentageHealthy"},
-		"AWS/SNS":              {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
-		"AWS/SQS":              {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
-		"AWS/S3":               {"BucketSizeBytes", "NumberOfObjects"},
-		"AWS/SWF":              {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut"},
-		"AWS/StorageGateway":   {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed", "CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
-		"AWS/WorkSpaces":       {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
+		"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"},
+		"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
+		"AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"},
+		"AWS/ElasticMapReduce": {"IsIdle", "JobsRunning", "JobsFailed",
+			"MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen",
+			"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers",
+			"S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "TotalLoad",
+			"BackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup",
+			"IsIdle", "ContainerAllocated", "ContainerReserved", "ContainerPending", "AppsCompleted", "AppsFailed", "AppsKilled", "AppsPending", "AppsRunning", "AppsSubmitted",
+			"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "MRTotalNodes", "MRActiveNodes", "MRLostNodes", "MRUnhealthyNodes", "MRDecommissionedNodes", "MRRebootedNodes",
+			"S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB",
+			"HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"},
+		"AWS/ES":       {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"},
+		"AWS/Kinesis":  {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"},
+		"AWS/Lambda":   {"Invocations", "Errors", "Duration", "Throttles"},
+		"AWS/ML":       {"PredictCount", "PredictFailureCount"},
+		"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
+		"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+		"AWS/RDS":      {"BinLogDiskUsage", "CPUUtilization", "CPUCreditUsage", "CPUCreditBalance", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"},
+		"AWS/Route53":  {"HealthCheckStatus", "HealthCheckPercentageHealthy"},
+		"AWS/SNS":      {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
+		"AWS/SQS":      {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
+		"AWS/S3":       {"BucketSizeBytes", "NumberOfObjects"},
+		"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
+			"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
+		"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
+			"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
+		"AWS/WAF":        {"AllowedRequests", "BlockedRequests", "CountedRequests"},
+		"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
 	}
 	dimensionsMap = map[string][]string{
 		"AWS/AutoScaling":      {"AutoScalingGroupName"},
@@ -47,13 +63,15 @@ func init() {
 		"AWS/CloudFront":       {"DistributionId", "Region"},
 		"AWS/CloudSearch":      {},
 		"AWS/DynamoDB":         {"TableName", "GlobalSecondaryIndexName", "Operation"},
+		"AWS/ECS":              {"ClusterName", "ServiceName"},
 		"AWS/ElastiCache":      {"CacheClusterId", "CacheNodeId"},
 		"AWS/EBS":              {"VolumeId"},
 		"AWS/EC2":              {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
-		"AWS/ECS":              {"ClusterName", "ServiceName"},
 		"AWS/ELB":              {"LoadBalancerName", "AvailabilityZone"},
-		"AWS/ElasticMapReduce": {"ClusterId", "JobId"},
+		"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
+		"AWS/ES":               {},
 		"AWS/Kinesis":          {"StreamName"},
+		"AWS/Lambda":           {"FunctionName"},
 		"AWS/ML":               {"MLModelId", "RequestMode"},
 		"AWS/OpsWorks":         {"StackId", "LayerId", "InstanceId"},
 		"AWS/Redshift":         {"NodeID", "ClusterIdentifier"},
@@ -62,8 +80,9 @@ func init() {
 		"AWS/SNS":              {"Application", "Platform", "TopicName"},
 		"AWS/SQS":              {"QueueName"},
 		"AWS/S3":               {"BucketName", "StorageType"},
-		"AWS/SWF":              {"Domain", "ActivityTypeName", "ActivityTypeVersion"},
+		"AWS/SWF":              {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
 		"AWS/StorageGateway":   {"GatewayId", "GatewayName", "VolumeId"},
+		"AWS/WAF":              {"Rule", "WebACL"},
 		"AWS/WorkSpaces":       {"DirectoryId", "WorkspaceId"},
 	}
 }
@@ -113,6 +132,7 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) {
 		c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
 		return
 	}
+	sort.Sort(sort.StringSlice(namespaceMetrics))
 
 	result := []interface{}{}
 	for _, name := range namespaceMetrics {
@@ -136,6 +156,7 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
 		c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
 		return
 	}
+	sort.Sort(sort.StringSlice(dimensionValues))
 
 	result := []interface{}{}
 	for _, name := range dimensionValues {

+ 8 - 0
pkg/api/dashboard_snapshot.go

@@ -12,6 +12,14 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 )
 
+func GetSharingOptions(c *middleware.Context) {
+	c.JSON(200, util.DynMap{
+		"externalSnapshotURL":  setting.ExternalSnapshotUrl,
+		"externalSnapshotName": setting.ExternalSnapshotName,
+		"externalEnabled":      setting.ExternalEnabled,
+	})
+}
+
 func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
 	if cmd.External {
 		// external snapshot ref requires key and delete key

+ 2 - 1
pkg/api/datasources.go

@@ -65,6 +65,7 @@ func GetDataSourceById(c *middleware.Context) Response {
 		BasicAuth:         ds.BasicAuth,
 		BasicAuthUser:     ds.BasicAuthUser,
 		BasicAuthPassword: ds.BasicAuthPassword,
+		WithCredentials:   ds.WithCredentials,
 		IsDefault:         ds.IsDefault,
 		JsonData:          ds.JsonData,
 	})
@@ -117,7 +118,7 @@ func GetDataSourcePlugins(c *middleware.Context) {
 	dsList := make(map[string]interface{})
 
 	for key, value := range plugins.DataSources {
-		if value.(map[string]interface{})["builtIn"] == nil {
+		if !value.BuiltIn {
 			dsList[key] = value
 		}
 	}

+ 25 - 0
pkg/api/dtos/index.go

@@ -0,0 +1,25 @@
+package dtos
+
+type IndexViewData struct {
+	User               *CurrentUser
+	Settings           map[string]interface{}
+	AppUrl             string
+	AppSubUrl          string
+	GoogleAnalyticsId  string
+	GoogleTagManagerId string
+
+	PluginCss    []*PluginCss
+	PluginJs     []string
+	MainNavLinks []*NavLink
+}
+
+type PluginCss struct {
+	Light string `json:"light"`
+	Dark  string `json:"dark"`
+}
+
+type NavLink struct {
+	Text string `json:"text"`
+	Icon string `json:"icon"`
+	Href string `json:"href"`
+}

+ 8 - 0
pkg/api/dtos/plugin_bundle.go

@@ -0,0 +1,8 @@
+package dtos
+
+type PluginBundle struct {
+	Type     string                 `json:"type"`
+	Enabled  bool                   `json:"enabled"`
+	Module   string                 `json:"module"`
+	JsonData map[string]interface{} `json:"jsonData"`
+}

+ 13 - 0
pkg/api/frontendsettings.go

@@ -62,6 +62,9 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 			if ds.BasicAuth {
 				dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword)
 			}
+			if ds.WithCredentials {
+				dsMap["withCredentials"] = ds.WithCredentials
+			}
 
 			if ds.Type == m.DS_INFLUXDB_08 {
 				dsMap["username"] = ds.User
@@ -106,11 +109,21 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 		defaultDatasource = "-- Grafana --"
 	}
 
+	panels := map[string]interface{}{}
+	for _, panel := range plugins.Panels {
+		panels[panel.Type] = map[string]interface{}{
+			"module": panel.Module,
+			"name":   panel.Name,
+		}
+	}
+
 	jsonObj := map[string]interface{}{
 		"defaultDatasource": defaultDatasource,
 		"datasources":       datasources,
+		"panels":            panels,
 		"appSubUrl":         setting.AppSubUrl,
 		"allowOrgCreate":    (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
+		"authProxyEnabled":  setting.AuthProxyEnabled,
 		"buildInfo": map[string]interface{}{
 			"version":    setting.BuildVersion,
 			"commit":     setting.BuildCommit,

+ 44 - 35
pkg/api/index.go

@@ -3,65 +3,74 @@ package api
 import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/middleware"
+	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
-func setIndexViewData(c *middleware.Context) error {
+func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 	settings, err := getFrontendSettingsMap(c)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	currentUser := &dtos.CurrentUser{
-		Id:             c.UserId,
-		IsSignedIn:     c.IsSignedIn,
-		Login:          c.Login,
-		Email:          c.Email,
-		Name:           c.Name,
-		LightTheme:     c.Theme == "light",
-		OrgId:          c.OrgId,
-		OrgName:        c.OrgName,
-		OrgRole:        c.OrgRole,
-		GravatarUrl:    dtos.GetGravatarUrl(c.Email),
-		IsGrafanaAdmin: c.IsGrafanaAdmin,
+	var data = dtos.IndexViewData{
+		User: &dtos.CurrentUser{
+			Id:             c.UserId,
+			IsSignedIn:     c.IsSignedIn,
+			Login:          c.Login,
+			Email:          c.Email,
+			Name:           c.Name,
+			LightTheme:     c.Theme == "light",
+			OrgId:          c.OrgId,
+			OrgName:        c.OrgName,
+			OrgRole:        c.OrgRole,
+			GravatarUrl:    dtos.GetGravatarUrl(c.Email),
+			IsGrafanaAdmin: c.IsGrafanaAdmin,
+		},
+		Settings:           settings,
+		AppUrl:             setting.AppUrl,
+		AppSubUrl:          setting.AppSubUrl,
+		GoogleAnalyticsId:  setting.GoogleAnalyticsId,
+		GoogleTagManagerId: setting.GoogleTagManagerId,
 	}
 
 	if setting.DisableGravatar {
-		currentUser.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png"
+		data.User.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png"
 	}
 
-	if len(currentUser.Name) == 0 {
-		currentUser.Name = currentUser.Login
+	if len(data.User.Name) == 0 {
+		data.User.Name = data.User.Login
 	}
 
 	themeUrlParam := c.Query("theme")
 	if themeUrlParam == "light" {
-		currentUser.LightTheme = true
+		data.User.LightTheme = true
 	}
 
-	c.Data["User"] = currentUser
-	c.Data["Settings"] = settings
-	c.Data["AppUrl"] = setting.AppUrl
-	c.Data["AppSubUrl"] = setting.AppSubUrl
+	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+		Text: "Dashboards",
+		Icon: "fa fa-fw fa-th-large",
+		Href: "/",
+	})
 
-	if setting.GoogleAnalyticsId != "" {
-		c.Data["GoogleAnalyticsId"] = setting.GoogleAnalyticsId
+	if c.OrgRole == m.ROLE_ADMIN {
+		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+			Text: "Data Sources",
+			Icon: "fa fa-fw fa-database",
+			Href: "/datasources",
+		})
 	}
 
-	if setting.GoogleTagManagerId != "" {
-		c.Data["GoogleTagManagerId"] = setting.GoogleTagManagerId
-	}
-
-	return nil
+	return &data, nil
 }
 
 func Index(c *middleware.Context) {
-	if err := setIndexViewData(c); err != nil {
+	if data, err := setIndexViewData(c); err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return
+	} else {
+		c.HTML(200, "index", data)
 	}
-
-	c.HTML(200, "index")
 }
 
 func NotFoundHandler(c *middleware.Context) {
@@ -70,10 +79,10 @@ func NotFoundHandler(c *middleware.Context) {
 		return
 	}
 
-	if err := setIndexViewData(c); err != nil {
+	if data, err := setIndexViewData(c); err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return
+	} else {
+		c.HTML(404, "index", data)
 	}
-
-	c.HTML(404, "index")
 }

+ 7 - 6
pkg/api/login.go

@@ -19,18 +19,19 @@ const (
 )
 
 func LoginView(c *middleware.Context) {
-	if err := setIndexViewData(c); err != nil {
+	viewData, err := setIndexViewData(c)
+	if err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return
 	}
 
-	settings := c.Data["Settings"].(map[string]interface{})
-	settings["googleAuthEnabled"] = setting.OAuthService.Google
-	settings["githubAuthEnabled"] = setting.OAuthService.GitHub
-	settings["disableUserSignUp"] = !setting.AllowUserSignUp
+	viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
+	viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
+	viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
+	viewData.Settings["loginHint"] = setting.LoginHint
 
 	if !tryLoginUsingRememberCookie(c) {
-		c.HTML(200, VIEW_INDEX)
+		c.HTML(200, VIEW_INDEX, viewData)
 		return
 	}
 

+ 15 - 8
pkg/cmd/web.go

@@ -14,6 +14,7 @@ import (
 	"github.com/grafana/grafana/pkg/api/static"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
+	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -28,12 +29,18 @@ func newMacaron() *macaron.Macaron {
 		m.Use(middleware.Gziper())
 	}
 
-	mapStatic(m, "", "public")
-	mapStatic(m, "app", "app")
-	mapStatic(m, "css", "css")
-	mapStatic(m, "img", "img")
-	mapStatic(m, "fonts", "fonts")
-	mapStatic(m, "robots.txt", "robots.txt")
+	for _, route := range plugins.StaticRoutes {
+		pluginRoute := path.Join("/public/plugins/", route.Url)
+		log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Path)
+		mapStatic(m, route.Path, "", pluginRoute)
+	}
+
+	mapStatic(m, setting.StaticRootPath, "", "public")
+	mapStatic(m, setting.StaticRootPath, "app", "app")
+	mapStatic(m, setting.StaticRootPath, "css", "css")
+	mapStatic(m, setting.StaticRootPath, "img", "img")
+	mapStatic(m, setting.StaticRootPath, "fonts", "fonts")
+	mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
 
 	m.Use(macaron.Renderer(macaron.RenderOptions{
 		Directory:  path.Join(setting.StaticRootPath, "views"),
@@ -51,7 +58,7 @@ func newMacaron() *macaron.Macaron {
 	return m
 }
 
-func mapStatic(m *macaron.Macaron, dir string, prefix string) {
+func mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
 	headers := func(c *macaron.Context) {
 		c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
 	}
@@ -63,7 +70,7 @@ func mapStatic(m *macaron.Macaron, dir string, prefix string) {
 	}
 
 	m.Use(httpstatic.Static(
-		path.Join(setting.StaticRootPath, dir),
+		path.Join(rootDir, dir),
 		httpstatic.StaticOptions{
 			SkipLogging: true,
 			Prefix:      prefix,

+ 95 - 0
pkg/log/syslog.go

@@ -0,0 +1,95 @@
+//+build !windows,!nacl,!plan9
+
+package log
+
+import (
+	"encoding/json"
+	"errors"
+	"log/syslog"
+)
+
+type SyslogWriter struct {
+	syslog   *syslog.Writer
+	Network  string `json:"network"`
+	Address  string `json:"address"`
+	Facility string `json:"facility"`
+	Tag      string `json:"tag"`
+}
+
+func NewSyslog() LoggerInterface {
+	return new(SyslogWriter)
+}
+
+func (sw *SyslogWriter) Init(config string) error {
+	if err := json.Unmarshal([]byte(config), sw); err != nil {
+		return err
+	}
+
+	prio, err := parseFacility(sw.Facility)
+	if err != nil {
+		return err
+	}
+
+	w, err := syslog.Dial(sw.Network, sw.Address, prio, sw.Tag)
+	if err != nil {
+		return err
+	}
+
+	sw.syslog = w
+	return nil
+}
+
+func (sw *SyslogWriter) WriteMsg(msg string, skip, level int) error {
+	var err error
+
+	switch level {
+	case TRACE, DEBUG:
+		err = sw.syslog.Debug(msg)
+	case INFO:
+		err = sw.syslog.Info(msg)
+	case WARN:
+		err = sw.syslog.Warning(msg)
+	case ERROR:
+		err = sw.syslog.Err(msg)
+	case CRITICAL:
+		err = sw.syslog.Crit(msg)
+	case FATAL:
+		err = sw.syslog.Alert(msg)
+	default:
+		err = errors.New("invalid syslog level")
+	}
+
+	return err
+}
+
+func (sw *SyslogWriter) Destroy() {
+	sw.syslog.Close()
+}
+
+func (sw *SyslogWriter) Flush() {}
+
+var facilities = map[string]syslog.Priority{
+	"user":   syslog.LOG_USER,
+	"daemon": syslog.LOG_DAEMON,
+	"local0": syslog.LOG_LOCAL0,
+	"local1": syslog.LOG_LOCAL1,
+	"local2": syslog.LOG_LOCAL2,
+	"local3": syslog.LOG_LOCAL3,
+	"local4": syslog.LOG_LOCAL4,
+	"local5": syslog.LOG_LOCAL5,
+	"local6": syslog.LOG_LOCAL6,
+	"local7": syslog.LOG_LOCAL7,
+}
+
+func parseFacility(facility string) (syslog.Priority, error) {
+	prio, ok := facilities[facility]
+	if !ok {
+		return syslog.LOG_LOCAL0, errors.New("invalid syslog facility")
+	}
+
+	return prio, nil
+}
+
+func init() {
+	Register("syslog", NewSyslog)
+}

+ 1 - 1
pkg/login/ldap.go

@@ -131,8 +131,8 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error)
 	}
 
 	return userQuery.Result, nil
-}
 
+}
 func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error) {
 	cmd := m.CreateUserCommand{
 		Login: ldapUser.Username,

+ 6 - 1
pkg/middleware/logger.go

@@ -32,7 +32,12 @@ func Logger() macaron.Handler {
 		rw := res.(macaron.ResponseWriter)
 		c.Next()
 
-		content := fmt.Sprintf("Completed %s %v %s in %v", req.URL.Path, rw.Status(), http.StatusText(rw.Status()), time.Since(start))
+		uname := c.GetCookie(setting.CookieUserName)
+		if len(uname) == 0 {
+			uname = "-"
+		}
+
+		content := fmt.Sprintf("Completed %s %s \"%s %s %s\" %v %s %d bytes in %dus", c.RemoteAddr(), uname, req.Method, req.URL.Path, req.Proto, rw.Status(), http.StatusText(rw.Status()), rw.Size(), time.Since(start)/time.Microsecond)
 
 		switch rw.Status() {
 		case 200, 304:

+ 3 - 0
pkg/models/datasource.go

@@ -40,6 +40,7 @@ type DataSource struct {
 	BasicAuth         bool
 	BasicAuthUser     string
 	BasicAuthPassword string
+	WithCredentials   bool
 	IsDefault         bool
 	JsonData          map[string]interface{}
 
@@ -83,6 +84,7 @@ type AddDataSourceCommand struct {
 	BasicAuth         bool                   `json:"basicAuth"`
 	BasicAuthUser     string                 `json:"basicAuthUser"`
 	BasicAuthPassword string                 `json:"basicAuthPassword"`
+	WithCredentials   bool                   `json:"withCredentials"`
 	IsDefault         bool                   `json:"isDefault"`
 	JsonData          map[string]interface{} `json:"jsonData"`
 
@@ -103,6 +105,7 @@ type UpdateDataSourceCommand struct {
 	BasicAuth         bool                   `json:"basicAuth"`
 	BasicAuthUser     string                 `json:"basicAuthUser"`
 	BasicAuthPassword string                 `json:"basicAuthPassword"`
+	WithCredentials   bool                   `json:"withCredentials"`
 	IsDefault         bool                   `json:"isDefault"`
 	JsonData          map[string]interface{} `json:"jsonData"`
 

+ 34 - 0
pkg/models/plugin_bundle.go

@@ -0,0 +1,34 @@
+package models
+
+import "time"
+
+type PluginBundle struct {
+	Id       int64
+	Type     string
+	OrgId    int64
+	Enabled  bool
+	JsonData map[string]interface{}
+
+	Created time.Time
+	Updated time.Time
+}
+
+// ----------------------
+// COMMANDS
+
+// Also acts as api DTO
+type UpdatePluginBundleCmd struct {
+	Type     string                 `json:"type" binding:"Required"`
+	Enabled  bool                   `json:"enabled"`
+	JsonData map[string]interface{} `json:"jsonData"`
+
+	Id    int64 `json:"-"`
+	OrgId int64 `json:"-"`
+}
+
+// ---------------------
+// QUERIES
+type GetPluginBundlesQuery struct {
+	OrgId  int64
+	Result []*PluginBundle
+}

+ 26 - 0
pkg/plugins/models.go

@@ -0,0 +1,26 @@
+package plugins
+
+type DataSourcePlugin struct {
+	Type               string                 `json:"type"`
+	Name               string                 `json:"name"`
+	ServiceName        string                 `json:"serviceName"`
+	Module             string                 `json:"module"`
+	Partials           map[string]interface{} `json:"partials"`
+	DefaultMatchFormat string                 `json:"defaultMatchFormat"`
+	Annotations        bool                   `json:"annotations"`
+	Metrics            bool                   `json:"metrics"`
+	BuiltIn            bool                   `json:"builtIn"`
+	StaticRootConfig   *StaticRootConfig      `json:"staticRoot"`
+}
+
+type PanelPlugin struct {
+	Type             string            `json:"type"`
+	Name             string            `json:"name"`
+	Module           string            `json:"module"`
+	StaticRootConfig *StaticRootConfig `json:"staticRoot"`
+}
+
+type StaticRootConfig struct {
+	Url  string `json:"url"`
+	Path string `json:"path"`
+}

+ 66 - 18
pkg/plugins/plugins.go

@@ -6,18 +6,17 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"strings"
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 
-type PluginMeta struct {
-	Type string `json:"type"`
-	Name string `json:"name"`
-}
-
 var (
-	DataSources map[string]interface{}
+	DataSources  map[string]DataSourcePlugin
+	Panels       map[string]PanelPlugin
+	StaticRoutes []*StaticRootConfig
 )
 
 type PluginScanner struct {
@@ -25,18 +24,37 @@ type PluginScanner struct {
 	errors     []error
 }
 
-func Init() {
+func Init() error {
+	DataSources = make(map[string]DataSourcePlugin)
+	StaticRoutes = make([]*StaticRootConfig, 0)
+	Panels = make(map[string]PanelPlugin)
+
 	scan(path.Join(setting.StaticRootPath, "app/plugins"))
+	scan(path.Join(setting.PluginsPath))
+	checkExternalPluginPaths()
+
+	return nil
 }
 
-func scan(pluginDir string) error {
-	DataSources = make(map[string]interface{})
+func checkExternalPluginPaths() error {
+	for _, section := range setting.Cfg.Sections() {
+		if strings.HasPrefix(section.Name(), "plugin.") {
+			path := section.Key("path").String()
+			if path != "" {
+				log.Info("Plugin: Scaning dir %s", path)
+				scan(path)
+			}
+		}
+	}
+	return nil
+}
 
+func scan(pluginDir string) error {
 	scanner := &PluginScanner{
 		pluginPath: pluginDir,
 	}
 
-	if err := filepath.Walk(pluginDir, scanner.walker); err != nil {
+	if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
 		return err
 	}
 
@@ -47,7 +65,7 @@ func scan(pluginDir string) error {
 	return nil
 }
 
-func (scanner *PluginScanner) walker(path string, f os.FileInfo, err error) error {
+func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
 	if err != nil {
 		return err
 	}
@@ -57,17 +75,25 @@ func (scanner *PluginScanner) walker(path string, f os.FileInfo, err error) erro
 	}
 
 	if f.Name() == "plugin.json" {
-		err := scanner.loadPluginJson(path)
+		err := scanner.loadPluginJson(currentPath)
 		if err != nil {
-			log.Error(3, "Failed to load plugin json file: %v,  err: %v", path, err)
+			log.Error(3, "Failed to load plugin json file: %v,  err: %v", currentPath, err)
 			scanner.errors = append(scanner.errors, err)
 		}
 	}
 	return nil
 }
 
-func (scanner *PluginScanner) loadPluginJson(path string) error {
-	reader, err := os.Open(path)
+func addStaticRoot(staticRootConfig *StaticRootConfig, currentDir string) {
+	if staticRootConfig != nil {
+		staticRootConfig.Path = path.Join(currentDir, staticRootConfig.Path)
+		StaticRoutes = append(StaticRoutes, staticRootConfig)
+	}
+}
+
+func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
+	currentDir := filepath.Dir(pluginJsonFilePath)
+	reader, err := os.Open(pluginJsonFilePath)
 	if err != nil {
 		return err
 	}
@@ -87,11 +113,33 @@ func (scanner *PluginScanner) loadPluginJson(path string) error {
 	}
 
 	if pluginType == "datasource" {
-		datasourceType, exists := pluginJson["type"]
-		if !exists {
+		p := DataSourcePlugin{}
+		reader.Seek(0, 0)
+		if err := jsonParser.Decode(&p); err != nil {
+			return err
+		}
+
+		if p.Type == "" {
+			return errors.New("Did not find type property in plugin.json")
+		}
+
+		DataSources[p.Type] = p
+		addStaticRoot(p.StaticRootConfig, currentDir)
+	}
+
+	if pluginType == "panel" {
+		p := PanelPlugin{}
+		reader.Seek(0, 0)
+		if err := jsonParser.Decode(&p); err != nil {
+			return err
+		}
+
+		if p.Type == "" {
 			return errors.New("Did not find type property in plugin.json")
 		}
-		DataSources[datasourceType.(string)] = pluginJson
+
+		Panels[p.Type] = p
+		addStaticRoot(p.StaticRootConfig, currentDir)
 	}
 
 	return nil

+ 5 - 2
pkg/plugins/plugins_test.go

@@ -4,14 +4,17 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
+	"gopkg.in/ini.v1"
 )
 
 func TestPluginScans(t *testing.T) {
 
 	Convey("When scaning for plugins", t, func() {
-		path, _ := filepath.Abs("../../public/app/plugins")
-		err := scan(path)
+		setting.StaticRootPath, _ = filepath.Abs("../../public/")
+		setting.Cfg = ini.Empty()
+		err := Init()
 
 		So(err, ShouldBeNil)
 		So(len(DataSources), ShouldBeGreaterThan, 1)

+ 2 - 0
pkg/services/sqlstore/datasource.go

@@ -114,12 +114,14 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 			BasicAuth:         cmd.BasicAuth,
 			BasicAuthUser:     cmd.BasicAuthUser,
 			BasicAuthPassword: cmd.BasicAuthPassword,
+			WithCredentials:   cmd.WithCredentials,
 			JsonData:          cmd.JsonData,
 			Updated:           time.Now(),
 		}
 
 		sess.UseBool("is_default")
 		sess.UseBool("basic_auth")
+		sess.UseBool("with_credentials")
 
 		_, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds)
 		if err != nil {

+ 5 - 0
pkg/services/sqlstore/migrations/datasource_mig.go

@@ -96,4 +96,9 @@ func addDataSourceMigration(mg *Migrator) {
 	}))
 
 	mg.AddMigration("Drop old table data_source_v1 #2", NewDropTableMigration("data_source_v1"))
+
+	// add column to activate withCredentials option
+	mg.AddMigration("Add column with_credentials", NewAddColumnMigration(tableV2, &Column{
+		Name: "with_credentials", Type: DB_Bool, Nullable: false, Default: "0",
+	}))
 }

+ 1 - 0
pkg/services/sqlstore/migrations/migrations.go

@@ -18,6 +18,7 @@ func AddMigrations(mg *Migrator) {
 	addApiKeyMigrations(mg)
 	addDashboardSnapshotMigrations(mg)
 	addQuotaMigration(mg)
+	addPluginBundleMigration(mg)
 }
 
 func addMigrationLogMigrations(mg *Migrator) {

+ 26 - 0
pkg/services/sqlstore/migrations/plugin_bundle.go

@@ -0,0 +1,26 @@
+package migrations
+
+import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addPluginBundleMigration(mg *Migrator) {
+
+	var pluginBundleV1 = Table{
+		Name: "plugin_bundle",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "org_id", Type: DB_BigInt, Nullable: true},
+			{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "enabled", Type: DB_Bool, Nullable: false},
+			{Name: "json_data", Type: DB_Text, Nullable: true},
+			{Name: "created", Type: DB_DateTime, Nullable: false},
+			{Name: "updated", Type: DB_DateTime, Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"org_id", "type"}, Type: UniqueIndex},
+		},
+	}
+	mg.AddMigration("create plugin_bundle table v1", NewAddTableMigration(pluginBundleV1))
+
+	//-------  indexes ------------------
+	addTableIndicesMigrations(mg, "v1", pluginBundleV1)
+}

+ 1 - 1
pkg/services/sqlstore/migrator/column.go

@@ -55,7 +55,7 @@ func (col *Column) StringNoPk(d Dialect) string {
 	}
 
 	if col.Default != "" {
-		sql += "DEFAULT " + col.Default + " "
+		sql += "DEFAULT " + d.Default(col) + " "
 	}
 
 	return sql

+ 6 - 1
pkg/services/sqlstore/migrator/dialect.go

@@ -17,10 +17,11 @@ type Dialect interface {
 	SqlType(col *Column) string
 	SupportEngine() bool
 	LikeStr() string
+	Default(col *Column) string
 
 	CreateIndexSql(tableName string, index *Index) string
 	CreateTableSql(table *Table) string
-	AddColumnSql(tableName string, Col *Column) string
+	AddColumnSql(tableName string, col *Column) string
 	CopyTableData(sourceTable string, targetTable string, sourceCols []string, targetCols []string) string
 	DropTable(tableName string) string
 	DropIndexSql(tableName string, index *Index) string
@@ -71,6 +72,10 @@ func (b *BaseDialect) EqStr() string {
 	return "="
 }
 
+func (b *BaseDialect) Default(col *Column) string {
+	return col.Default
+}
+
 func (b *BaseDialect) CreateTableSql(table *Table) string {
 	var sql string
 	sql = "CREATE TABLE IF NOT EXISTS "

+ 4 - 0
pkg/services/sqlstore/migrator/migrations.go

@@ -64,6 +64,10 @@ type AddColumnMigration struct {
 	column    *Column
 }
 
+func NewAddColumnMigration(table Table, col *Column) *AddColumnMigration {
+	return &AddColumnMigration{tableName: table.Name, column: col}
+}
+
 func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration {
 	m.tableName = tableName
 	return m

+ 11 - 0
pkg/services/sqlstore/migrator/postgres_dialect.go

@@ -36,6 +36,17 @@ func (db *Postgres) AutoIncrStr() string {
 	return ""
 }
 
+func (b *Postgres) Default(col *Column) string {
+	if col.Type == DB_Bool {
+		if col.Default == "0" {
+			return "FALSE"
+		} else {
+			return "TRUE"
+		}
+	}
+	return col.Default
+}
+
 func (db *Postgres) SqlType(c *Column) string {
 	var res string
 	switch t := c.Type; t {

+ 46 - 0
pkg/services/sqlstore/plugin_bundle.go

@@ -0,0 +1,46 @@
+package sqlstore
+
+import (
+	"time"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+	bus.AddHandler("sql", GetPluginBundles)
+	bus.AddHandler("sql", UpdatePluginBundle)
+}
+
+func GetPluginBundles(query *m.GetPluginBundlesQuery) error {
+	sess := x.Where("org_id=?", query.OrgId)
+
+	query.Result = make([]*m.PluginBundle, 0)
+	return sess.Find(&query.Result)
+}
+
+func UpdatePluginBundle(cmd *m.UpdatePluginBundleCmd) error {
+	return inTransaction2(func(sess *session) error {
+		var bundle m.PluginBundle
+
+		exists, err := sess.Where("org_id=? and type=?", cmd.OrgId, cmd.Type).Get(&bundle)
+		sess.UseBool("enabled")
+		if !exists {
+			bundle = m.PluginBundle{
+				Type:     cmd.Type,
+				OrgId:    cmd.OrgId,
+				Enabled:  cmd.Enabled,
+				JsonData: cmd.JsonData,
+				Created:  time.Now(),
+				Updated:  time.Now(),
+			}
+			_, err = sess.Insert(&bundle)
+			return err
+		} else {
+			bundle.Enabled = cmd.Enabled
+			bundle.JsonData = cmd.JsonData
+			_, err = sess.Id(bundle.Id).Update(&bundle)
+			return err
+		}
+	})
+}

+ 29 - 7
pkg/setting/setting.go

@@ -48,9 +48,10 @@ var (
 	BuildStamp   int64
 
 	// Paths
-	LogsPath string
-	HomePath string
-	DataPath string
+	LogsPath    string
+	HomePath    string
+	DataPath    string
+	PluginsPath string
 
 	// Log settings.
 	LogModes   []string
@@ -76,12 +77,18 @@ var (
 	EmailCodeValidMinutes int
 	DataProxyWhiteList    map[string]bool
 
+	// Snapshots
+	ExternalSnapshotUrl  string
+	ExternalSnapshotName string
+	ExternalEnabled      bool
+
 	// User settings
 	AllowUserSignUp    bool
 	AllowUserOrgCreate bool
 	AutoAssignOrg      bool
 	AutoAssignOrgRole  string
 	VerifyEmailEnabled bool
+	LoginHint          string
 
 	// Http auth
 	AdminUser     string
@@ -281,13 +288,11 @@ func loadSpecifedConfigFile(configFile string) {
 
 			defaultSec, err := Cfg.GetSection(section.Name())
 			if err != nil {
-				log.Error(3, "Unknown config section %s defined in %s", section.Name(), configFile)
-				continue
+				defaultSec, _ = Cfg.NewSection(section.Name())
 			}
 			defaultKey, err := defaultSec.GetKey(key.Name())
 			if err != nil {
-				log.Error(3, "Unknown config key %s defined in section %s, in file %s", key.Name(), section.Name(), configFile)
-				continue
+				defaultKey, _ = defaultSec.NewKey(key.Name(), key.Value())
 			}
 			defaultKey.SetValue(key.Value())
 		}
@@ -389,6 +394,7 @@ func NewConfigContext(args *CommandLineArgs) error {
 	loadConfiguration(args)
 
 	Env = Cfg.Section("").Key("app_mode").MustString("development")
+	PluginsPath = Cfg.Section("paths").Key("plugins").String()
 
 	server := Cfg.Section("server")
 	AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)
@@ -420,6 +426,12 @@ func NewConfigContext(args *CommandLineArgs) error {
 	CookieRememberName = security.Key("cookie_remember_name").String()
 	DisableGravatar = security.Key("disable_gravatar").MustBool(true)
 
+	// read snapshots settings
+	snapshots := Cfg.Section("snapshots")
+	ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String()
+	ExternalSnapshotName = snapshots.Key("external_snapshot_name").String()
+	ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
+
 	//  read data source proxy white list
 	DataProxyWhiteList = make(map[string]bool)
 	for _, hostAndIp := range security.Key("data_source_proxy_whitelist").Strings(" ") {
@@ -436,6 +448,7 @@ func NewConfigContext(args *CommandLineArgs) error {
 	AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
 	AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"})
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
+	LoginHint = users.Key("login_hint").String()
 
 	// anonymous access
 	AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
@@ -573,6 +586,14 @@ func initLogging(args *CommandLineArgs) {
 				"driver": sec.Key("driver").String(),
 				"conn":   sec.Key("conn").String(),
 			}
+		case "syslog":
+			LogConfigs[i] = util.DynMap{
+				"level":    level,
+				"network":  sec.Key("network").MustString(""),
+				"address":  sec.Key("address").MustString(""),
+				"facility": sec.Key("facility").MustString("local7"),
+				"tag":      sec.Key("tag").MustString(""),
+			}
 		}
 
 		cfgJsonBytes, _ := json.Marshal(LogConfigs[i])
@@ -607,6 +628,7 @@ func LogConfigurationInfo() {
 	text.WriteString(fmt.Sprintf("  home: %s\n", HomePath))
 	text.WriteString(fmt.Sprintf("  data: %s\n", DataPath))
 	text.WriteString(fmt.Sprintf("  logs: %s\n", LogsPath))
+	text.WriteString(fmt.Sprintf("  plugins: %s\n", PluginsPath))
 
 	log.Info(text.String())
 }

+ 98 - 0
pkg/util/filepath.go

@@ -0,0 +1,98 @@
+package util
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+)
+
+//WalkSkipDir is the Error returned when we want to skip descending into a directory
+var WalkSkipDir = errors.New("skip this directory")
+
+//WalkFunc is a callback function called for each path as a directory is walked
+//If resolvedPath != "", then we are following symbolic links.
+type WalkFunc func(resolvedPath string, info os.FileInfo, err error) error
+
+//Walk walks a path, optionally following symbolic links, and for each path,
+//it calls the walkFn passed.
+//
+//It is similar to filepath.Walk, except that it supports symbolic links and
+//can detect infinite loops while following sym links.
+//It solves the issue where your WalkFunc needs a path relative to the symbolic link
+//(resolving links within walkfunc loses the path to the symbolic link for each traversal).
+func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, walkFn WalkFunc) error {
+	info, err := os.Lstat(path)
+	if err != nil {
+		return err
+	}
+	var symlinkPathsFollowed map[string]bool
+	var resolvedPath string
+	if followSymlinks {
+		resolvedPath = path
+		if detectSymlinkInfiniteLoop {
+			symlinkPathsFollowed = make(map[string]bool, 8)
+		}
+	}
+	return walk(path, info, resolvedPath, symlinkPathsFollowed, walkFn)
+}
+
+//walk walks the path. It is a helper/sibling function to Walk.
+//It takes a resolvedPath into consideration. This way, paths being walked are
+//always relative to the path argument, even if symbolic links were resolved).
+//
+//If resolvedPath is "", then we are not following symbolic links.
+//If symlinkPathsFollowed is not nil, then we need to detect infinite loop.
+func walk(path string, info os.FileInfo, resolvedPath string,
+	symlinkPathsFollowed map[string]bool, walkFn WalkFunc) error {
+	if info == nil {
+		return errors.New("Walk: Nil FileInfo passed")
+	}
+	err := walkFn(resolvedPath, info, nil)
+	if err != nil {
+		if info.IsDir() && err == WalkSkipDir {
+			err = nil
+		}
+		return err
+	}
+	if resolvedPath != "" && info.Mode()&os.ModeSymlink == os.ModeSymlink {
+		path2, err := os.Readlink(resolvedPath)
+		if err != nil {
+			return err
+		}
+		//vout("SymLink Path: %v, links to: %v", resolvedPath, path2)
+		if symlinkPathsFollowed != nil {
+			if _, ok := symlinkPathsFollowed[path2]; ok {
+				errMsg := "Potential SymLink Infinite Loop. Path: %v, Link To: %v"
+				return fmt.Errorf(errMsg, resolvedPath, path2)
+			} else {
+				symlinkPathsFollowed[path2] = true
+			}
+		}
+		info2, err := os.Lstat(path2)
+		if err != nil {
+			return err
+		}
+		return walk(path, info2, path2, symlinkPathsFollowed, walkFn)
+	}
+	if info.IsDir() {
+		list, err := ioutil.ReadDir(path)
+		if err != nil {
+			return walkFn(resolvedPath, info, err)
+		}
+		for _, fileInfo := range list {
+			path2 := filepath.Join(path, fileInfo.Name())
+			var resolvedPath2 string
+			if resolvedPath != "" {
+				resolvedPath2 = filepath.Join(resolvedPath, fileInfo.Name())
+			}
+			err = walk(path2, fileInfo, resolvedPath2, symlinkPathsFollowed, walkFn)
+			if err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+	return nil
+}

+ 11 - 6
public/app/app.js

@@ -2,6 +2,7 @@ define([
   'angular',
   'jquery',
   'lodash',
+  'app/core/config',
   'require',
   'bootstrap',
   'angular-route',
@@ -12,7 +13,7 @@ define([
   'bindonce',
   'app/core/core',
 ],
-function (angular, $, _, appLevelRequire) {
+function (angular, $, _, config, appLevelRequire) {
   "use strict";
 
   var app = angular.module('grafana', []);
@@ -35,6 +36,8 @@ function (angular, $, _, appLevelRequire) {
     } else {
       _.extend(module, register_fns);
     }
+    // push it into the apps dependencies
+    apps_deps.push(module.name);
     return module;
   };
 
@@ -64,13 +67,15 @@ function (angular, $, _, appLevelRequire) {
     var module_name = 'grafana.'+type;
     // create the module
     app.useModule(angular.module(module_name, []));
-    // push it into the apps dependencies
-    apps_deps.push(module_name);
   });
 
-  var preBootRequires = [
-    'app/features/all',
-  ];
+  var preBootRequires = ['app/features/all'];
+  var pluginModules = config.bootData.pluginModules || [];
+
+  // add plugin modules
+  for (var i = 0; i < pluginModules.length; i++) {
+    preBootRequires.push(pluginModules[i]);
+  }
 
   app.boot = function() {
     require(preBootRequires, function () {

+ 1 - 0
public/app/core/config.js

@@ -6,6 +6,7 @@ function (Settings) {
 
   var bootData = window.grafanaBootData || { settings: {} };
   var options = bootData.settings;
+  options.bootData = bootData;
 
   return new Settings(options);
 

+ 1 - 0
public/app/core/controllers/login_ctrl.js

@@ -18,6 +18,7 @@ function (angular, coreModule, config) {
     $scope.googleAuthEnabled = config.googleAuthEnabled;
     $scope.githubAuthEnabled = config.githubAuthEnabled;
     $scope.disableUserSignUp = config.disableUserSignUp;
+    $scope.loginHint     = config.loginHint;
 
     $scope.loginMode = true;
     $scope.submitBtnText = 'Log in';

+ 6 - 11
public/app/core/controllers/sidemenu_ctrl.js

@@ -15,19 +15,13 @@ function (angular, _, $, coreModule, config) {
     };
 
     $scope.setupMainNav = function() {
-      $scope.mainLinks.push({
-        text: "Dashboards",
-        icon: "fa fa-fw fa-th-large",
-        href: $scope.getUrl("/"),
-      });
-
-      if (contextSrv.hasRole('Admin')) {
+      _.each(config.bootData.mainNavLinks, function(item) {
         $scope.mainLinks.push({
-          text: "Data Sources",
-          icon: "fa fa-fw fa-database",
-          href: $scope.getUrl("/datasources"),
+          text: item.text,
+          icon: item.icon,
+          href: $scope.getUrl(item.href)
         });
-      }
+      });
     };
 
     $scope.loadOrgs = function() {
@@ -120,6 +114,7 @@ function (angular, _, $, coreModule, config) {
     };
 
     $scope.init = function() {
+      $scope.showSignout = contextSrv.isSignedIn && !config['authProxyEnabled'];
       $scope.updateMenu();
       $scope.$on('$routeChangeSuccess', $scope.updateMenu);
     };

+ 1 - 1
public/app/core/directives/value_select_dropdown.js

@@ -156,7 +156,7 @@ function (angular, _, coreModule) {
     vm.selectionsChanged = function(commitChange) {
       vm.selectedValues = _.filter(vm.options, {selected: true});
 
-      if (vm.selectedValues.length > 1 && vm.selectedValues.length !== vm.options.length) {
+      if (vm.selectedValues.length > 1) {
         if (vm.selectedValues[0].text === 'All') {
           vm.selectedValues[0].selected = false;
           vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length);

+ 13 - 0
public/app/core/routes/all.js

@@ -131,6 +131,19 @@ define([
         templateUrl: 'app/partials/reset_password.html',
         controller : 'ResetPasswordCtrl',
       })
+      .when('/plugins', {
+        templateUrl: 'app/features/org/partials/plugins.html',
+        controller: 'PluginsCtrl',
+        resolve: loadOrgBundle,
+      })
+      .when('/plugins/edit/:type', {
+        templateUrl: 'app/features/org/partials/pluginEdit.html',
+        controller: 'PluginEditCtrl',
+        resolve: loadOrgBundle,
+      })
+      .when('/global-alerts', {
+        templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
+      })
       .otherwise({
         templateUrl: 'app/partials/error.html',
         controller: 'ErrorCtrl'

+ 2 - 2
public/app/core/services/context_srv.js

@@ -12,8 +12,8 @@ function (angular, _, coreModule, store, config) {
     var self = this;
 
     function User() {
-      if (window.grafanaBootData.user) {
-        _.extend(this, window.grafanaBootData.user);
+      if (config.bootData.user) {
+        _.extend(this, config.bootData.user);
       }
     }
 

+ 1 - 8
public/app/core/settings.js

@@ -8,15 +8,8 @@ function (_) {
     var defaults = {
       datasources                   : {},
       window_title_prefix           : 'Grafana - ',
-      panels                        : {
-        'graph':      { path: 'app/panels/graph',      name: 'Graph' },
-        'table':      { path: 'app/panels/table',      name: 'Table' },
-        'singlestat': { path: 'app/panels/singlestat', name: 'Single stat' },
-        'text':       { path: 'app/panels/text',       name: 'Text' },
-        'dashlist':   { path: 'app/panels/dashlist',   name: 'Dashboard list' },
-      },
+      panels                        : {},
       new_panel_title: 'Panel Title',
-      plugins: {},
       playlist_timespan: "1m",
       unsaved_changes_warning: true,
       appSubUrl: ""

+ 4 - 2
public/app/core/utils/kbn.js

@@ -399,6 +399,7 @@ function($, _) {
   // Volume
   kbn.valueFormats.litre  = kbn.formatBuilders.decimalSIPrefix('L');
   kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1);
+  kbn.valueFormats.m3     = kbn.formatBuilders.decimalSIPrefix('m3');
 
   // Time
   kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
@@ -626,8 +627,9 @@ function($, _) {
       {
         text: 'volume',
         submenu: [
-          {text: 'millilitre', value: 'mlitre'},
-          {text: 'litre',      value: 'litre' },
+          {text: 'millilitre',  value: 'mlitre'},
+          {text: 'litre',       value: 'litre' },
+          {text: 'cubic metre', value: 'm3'    },
         ]
       },
       {

+ 2 - 2
public/app/core/utils/rangeutil.ts

@@ -17,9 +17,9 @@ var spans = {
 
 var rangeOptions = [
   { from: 'now/d',    to: 'now/d',    display: 'Today',                 section: 2 },
-  { from: 'now/d',    to: 'now',      display: 'The day so far',        section: 2 },
+  { from: 'now/d',    to: 'now',      display: 'Today so far',          section: 2 },
   { from: 'now/w',    to: 'now/w',    display: 'This week',             section: 2 },
-  { from: 'now/w',    to: 'now',      display: 'Week to date',          section: 2 },
+  { from: 'now/w',    to: 'now',      display: 'This week so far',           section: 2 },
   { from: 'now/M',    to: 'now/M',    display: 'This month',            section: 2 },
   { from: 'now/y',    to: 'now/y',    display: 'This year',             section: 2 },
 

+ 26 - 24
public/app/features/admin/partials/orgs.html

@@ -4,33 +4,35 @@
 	</ul>
 </topnav>
 
-<div class="page-container">
-	<div class="page">
+<div class="page-container" style="background: transparent; border: 0;">
+	<div class="page-wide">
 		<h2>
 			Organizations
 		</h2>
-
-		<table class="grafana-options-table">
-			<tr>
-				<th style="text-align:left">Id</th>
-				<th>Name</th>
-				<th></th>
-			</tr>
-			<tr ng-repeat="org in orgs">
-        <td>{{org.id}}</td>
-				<td>{{org.name}}</td>
-				<td style="width: 1%">
-					<a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small">
-						<i class="fa fa-edit"></i>
-						Edit
-					</a>
-					&nbsp;&nbsp;
-					<a ng-click="deleteOrg(org)" class="btn btn-danger btn-small">
-						<i class="fa fa-remove"></i>
-					</a>
-				</td>
-			</tr>
+    <table class="filter-table form-inline">
+			<thead>
+				<tr>
+					<th>Id</th>
+					<th>Name</th>
+          <th></th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr ng-repeat="org in orgs">
+          <td>{{org.id}}</td>
+					<td>{{org.name}}</td>
+					<td class="text-right">
+						<a href="admin/orgs/edit/{{org.id}}" class="btn btn-inverse btn-small">
+							<i class="fa fa-edit"></i>
+							Edit
+						</a>
+						&nbsp;&nbsp;
+						<a ng-click="deleteOrg(org)" class="btn btn-danger btn-small">
+							<i class="fa fa-remove"></i>
+						</a>
+					</td>
+				</tr>
+			</tbody>
 		</table>
-
 	</div>
 </div>

+ 32 - 28
public/app/features/admin/partials/users.html

@@ -5,38 +5,42 @@
 	</ul>
 </topnav>
 
-<div class="page-container">
-	<div class="page">
+<div class="page-container" style="background: transparent; border: 0;">
+	<div class="page-wide">
 		<h2>
 			Users
 		</h2>
 
-		<table class="grafana-options-table">
-			<tr>
-				<th style="text-align:left">Id</th>
-				<th>Name</th>
-				<th>Login</th>
-				<th>Email</th>
-				<th style="white-space: nowrap">Grafana Admin</th>
-				<th></th>
-			</tr>
-			<tr ng-repeat="user in users">
-				<td>{{user.id}}</td>
-				<td>{{user.name}}</td>
-				<td>{{user.login}}</td>
-				<td>{{user.email}}</td>
-				<td>{{user.isAdmin}}</td>
-				<td style="width: 1%">
-					<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
-						<i class="fa fa-edit"></i>
-						Edit
-					</a>
-					&nbsp;&nbsp;
-					<a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
-						<i class="fa fa-remove"></i>
-					</a>
-				</td>
-			</tr>
+    <table class="filter-table form-inline">
+			<thead>
+				<tr>
+					<th>Id</th>
+					<th>Name</th>
+					<th>Login</th>
+					<th>Email</th>
+					<th style="white-space: nowrap">Grafana Admin</th>
+					<th></th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr ng-repeat="user in users">
+					<td>{{user.id}}</td>
+					<td>{{user.name}}</td>
+					<td>{{user.login}}</td>
+					<td>{{user.email}}</td>
+					<td>{{user.isAdmin}}</td>
+					<td class="text-right">
+						<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
+							<i class="fa fa-edit"></i>
+							Edit
+						</a>
+						&nbsp;&nbsp;
+						<a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
+							<i class="fa fa-remove"></i>
+						</a>
+					</td>
+				</tr>
+			</tbody>
 		</table>
 	</div>
 </div>

+ 6 - 1
public/app/features/dashboard/dashboardCtrl.js

@@ -2,8 +2,9 @@ define([
   'angular',
   'jquery',
   'app/core/config',
+  'moment',
 ],
-function (angular, $, config) {
+function (angular, $, config, moment) {
   "use strict";
 
   var module = angular.module('grafana.controllers');
@@ -149,6 +150,10 @@ function (angular, $, config) {
       });
     };
 
+    $scope.formatDate = function(date) {
+      return moment(date).format('MMM Do YYYY, h:mm:ss a');
+    };
+
   });
 
 });

+ 15 - 0
public/app/features/dashboard/dashboardNavCtrl.js

@@ -49,6 +49,21 @@ function (angular, _) {
       $scope.appEvent('hide-dash-search');
     };
 
+    $scope.makeEditable = function() {
+      $scope.dashboard.editable = true;
+
+      var clone = $scope.dashboard.getSaveModelClone();
+
+      backendSrv.saveDashboard(clone, {overwrite: false}).then(function(data) {
+        $scope.dashboard.version = data.version;
+        $scope.appEvent('dashboard-saved', $scope.dashboard);
+        $scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]);
+
+        //force refresh whole page
+        window.location.href = window.location.href;
+      }, $scope.handleSaveDashError);
+    };
+
     $scope.saveDashboard = function(options) {
       if ($scope.dashboardMeta.canSave === false) {
         return;

+ 9 - 4
public/app/features/dashboard/dashboardSrv.js

@@ -214,10 +214,7 @@ function (angular, $, _, moment) {
     };
 
     p.formatDate = function(date, format) {
-      if (!moment.isMoment(date)) {
-        date = moment(date);
-      }
-
+      date = moment.isMoment(date) ? date : moment(date);
       format = format || 'YYYY-MM-DD HH:mm:ss';
 
       return this.timezone === 'browser' ?
@@ -225,6 +222,14 @@ function (angular, $, _, moment) {
         moment.utc(date).format(format);
     };
 
+    p.getRelativeTime = function(date) {
+      date = moment.isMoment(date) ? date : moment(date);
+
+      return this.timezone === 'browser' ?
+        moment(date).fromNow() :
+        moment.utc(date).fromNow();
+    };
+
     p._updateSchema = function(old) {
       var i, j, k;
       var oldVersion = this.schemaVersion;

+ 1 - 0
public/app/features/dashboard/partials/dashboardTopNav.html

@@ -37,6 +37,7 @@
 						<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
 						<li><a class="pointer" ng-click="exportDashboard();">Export</a></li>
 						<li><a class="pointer" ng-click="editJson();">View JSON</a></li>
+						<li ng-if="contextSrv.isEditor && !dashboard.editable"><a class="pointer" ng-click="makeEditable();">Make Editable</a></li>
 						<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
 						<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
 					</ul>

+ 282 - 0
public/app/features/dashboard/partials/globalAlerts.html

@@ -0,0 +1,282 @@
+<topnav title="Alerting" subnav="false">
+  <ul class="nav">
+    <li class="active" ><a href="global-alerts">Global Alerts</a></li>
+  </ul>
+</topnav>
+
+<div class="page-container" style="background: transparent; border: 0;">
+  <div class="page-wide">
+    <h2>Global alerts</h2>
+
+    <div class="filter-controls-filters">
+      <div class="tight-form last">
+        <ul class="tight-form-list">
+          <li class="tight-form-item">Filters:</li>
+          <li class="tight-form-item">Alert State</li>
+          <li><!-- <value-select-dropdown></value-select-dropdown> --></li>
+          <li class="tight-form-item">Dashboards</li>
+          <li><!-- <value-select-dropdown></value-select-dropdown> --></li>
+          <li class="tight-form-item">
+            <a class="pointer">
+              <i class="fa fa-pencil"></i>
+            </a>
+          </li>
+        </ul>
+        <div class="clearfix"></div>
+      </div>
+    </div>
+    <ul class="filter-controls-actions">
+      <li>
+        <div class="dropdown">
+          <button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
+            <input class="cr1" id="state-enabled" type="checkbox">
+            <label for="state-enabled" class="cr1"></label> <span class="caret"></span>
+          </button>
+          <ul class="dropdown-menu" role="menu">
+            <li><a>All</a></li>
+          </ul>
+        </div>
+      </li>
+      <li>
+        <div class="dropdown">
+          <button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
+            Bulk Actions &nbsp; <span class="caret"></span>
+          </button>
+          <ul class="dropdown-menu" role="menu">
+            <li><a>Update notifications</a></li>
+          </ul>
+        </div>
+      </li>
+      <li>
+        <button class="btn btn-inverse" data-toggle="dropdown">
+          <i class="fa fa-fw fa-th-large"></i> New Dashboard from selected
+        </button>
+      </li>
+      <li>
+        <span class="filter-controls-actions-selected">2 selected, showing 6 of 6 total</span>
+      </li>
+    </ul>
+    <ul class="filter-list">
+      <li>
+        <ul class="filter-list-card">
+          <li class="filter-list-card-select">
+            <input class="cr1" id="alert1" type="checkbox">
+            <label for="alert1" class="cr1"></label>
+          </li>
+          <li>
+            <div class="filter-list-card-controls">
+              <div class="filter-list-card-links">
+                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Super Sekret</a></span>
+                <span class="filter-list-card-link">Panel: <a href="">Prod CPU Data Writes</a></span>
+              </div>
+              <div class="filter-list-card-config">
+                <a href="#"><i class="fa fa-cog"></i></a>
+              </div>
+              <div class="filter-list-card-expand" ng-click="alert1.expanded = !alert1.expanded">
+                <i class="fa fa-angle-right" ng-show="!alert1.expanded"></i>
+                <i class="fa fa-angle-down" ng-show="alert1.expanded"></i>
+              </div>
+            </div>
+            <span class="filter-list-card-title">Prod CPU Data Writes</span>
+            <span class="filter-list-card-status">
+              <span class="filter-list-card-state online">Online</span> for 19 hours
+            </span>
+          </li>
+        </ul>
+        <div class="filter-list-card-details" ng-show="alert1.expanded">
+          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
+          <div class="tight-form last">
+            <ul class="tight-form-list">
+              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
+              <li class="tight-form-item">apps</li>
+              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
+              <li class="tight-form-item">fakesite</li>
+              <li class="tight-form-item">counters</li>
+              <li class="tight-form-item">requests</li>
+              <li class="tight-form-item">count</li>
+              <li class="tight-form-item">scaleToSeconds(1)</li>
+              <li class="tight-form-item">aliasByNode(2)</li>
+            </ul>
+            <div class="clearfix"></div>
+          </div>
+        </div>
+      </li>
+      <li>
+        <ul class="filter-list-card">
+          <li class="filter-list-card-select">
+            <input class="cr1" id="alert2" type="checkbox" checked>
+            <label for="alert2" class="cr1"></label>
+          </li>
+          <li>
+            <div class="filter-list-card-controls">
+              <div class="filter-list-card-links">
+                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Insanely Super Duper Sekret</a></span>
+                <span class="filter-list-card-link">Panel: <a href="">client side full page load</a></span>
+              </div>
+              <div class="filter-list-card-config">
+                <a href="#"><i class="fa fa-cog"></i></a>
+              </div>
+              <div class="filter-list-card-expand" ng-click="alert2.expanded = !alert2.expanded">
+                <i class="fa fa-angle-right" ng-show="!alert2.expanded"></i>
+                <i class="fa fa-angle-down" ng-show="alert2.expanded"></i>
+              </div>
+            </div>
+            <span class="filter-list-card-title">Prod DB Reads</span>
+            <span class="filter-list-card-status">
+              <span class="filter-list-card-state warn">Warn</span> for 1 hour
+            </span>
+          </li>
+        </ul>
+        <div class="filter-list-card-details" ng-show="alert2.expanded">
+          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
+          <div class="tight-form last">
+            <ul class="tight-form-list">
+              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
+              <li class="tight-form-item">apps</li>
+              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
+              <li class="tight-form-item">fakesite</li>
+              <li class="tight-form-item">counters</li>
+              <li class="tight-form-item">requests</li>
+              <li class="tight-form-item">count</li>
+              <li class="tight-form-item">scaleToSeconds(1)</li>
+              <li class="tight-form-item">aliasByNode(2)</li>
+            </ul>
+            <div class="clearfix"></div>
+          </div>
+        </div>
+      </li>
+      <li>
+        <ul class="filter-list-card">
+          <li class="filter-list-card-select">
+            <input class="cr1" id="alert3" type="checkbox" checked>
+            <label for="alert3" class="cr1"></label>
+          </li>
+          <li>
+            <div class="filter-list-card-controls">
+              <div class="filter-list-card-links">
+                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Mildly Sekret</a></span>
+                <span class="filter-list-card-link">Panel: <a href="">Memory/CPU</a></span>
+              </div>
+              <div class="filter-list-card-config">
+                <a href="#"><i class="fa fa-cog"></i></a>
+              </div>
+              <div class="filter-list-card-expand" ng-click="alert3.expanded = !alert3.expanded">
+                <i class="fa fa-angle-right" ng-show="!alert3.expanded"></i>
+                <i class="fa fa-angle-down" ng-show="alert3.expanded"></i>
+              </div>
+            </div>
+            <span class="filter-list-card-title">Prod CPU Data Writes</span>
+            <span class="filter-list-card-status">
+              <span class="filter-list-card-state critical">Online</span> for 10 minutes
+            </span>
+          </li>
+        </ul>
+        <div class="filter-list-card-details" ng-show="alert3.expanded">
+          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
+          <div class="tight-form last">
+            <ul class="tight-form-list">
+              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
+              <li class="tight-form-item">apps</li>
+              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
+              <li class="tight-form-item">fakesite</li>
+              <li class="tight-form-item">counters</li>
+              <li class="tight-form-item">requests</li>
+              <li class="tight-form-item">count</li>
+              <li class="tight-form-item">scaleToSeconds(1)</li>
+              <li class="tight-form-item">aliasByNode(2)</li>
+            </ul>
+            <div class="clearfix"></div>
+          </div>
+        </div>
+      </li>
+      <li>
+        <ul class="filter-list-card">
+          <li class="filter-list-card-select">
+            <input class="cr1" id="alert4" type="checkbox">
+            <label for="alert4" class="cr1"></label>
+          </li>
+          <li>
+            <div class="filter-list-card-controls">
+              <div class="filter-list-card-links">
+                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Super Sekret</a></span>
+                <span class="filter-list-card-link">Panel: <a href="">Stacked lines</a></span>
+              </div>
+              <div class="filter-list-card-config">
+                <a href="#"><i class="fa fa-cog"></i></a>
+              </div>
+              <div class="filter-list-card-expand" ng-click="alert4.expanded = !alert4.expanded">
+                <i class="fa fa-angle-right" ng-show="!alert4.expanded"></i>
+                <i class="fa fa-angle-down" ng-show="alert4.expanded"></i>
+              </div>
+            </div>
+            <span class="filter-list-card-title">Critical Thing</span>
+            <span class="filter-list-card-status">
+              <span class="filter-list-card-state online">Online</span> for 5 weeks
+            </span>
+          </li>
+        </ul>
+        <div class="filter-list-card-details" ng-show="alert4.expanded">
+          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
+          <div class="tight-form last">
+            <ul class="tight-form-list">
+              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
+              <li class="tight-form-item">apps</li>
+              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
+              <li class="tight-form-item">fakesite</li>
+              <li class="tight-form-item">counters</li>
+              <li class="tight-form-item">requests</li>
+              <li class="tight-form-item">count</li>
+              <li class="tight-form-item">scaleToSeconds(1)</li>
+              <li class="tight-form-item">aliasByNode(2)</li>
+            </ul>
+            <div class="clearfix"></div>
+          </div>
+        </div>
+      </li>
+      <li>
+        <ul class="filter-list-card">
+          <li class="filter-list-card-select">
+            <input class="cr1" id="alert5" type="checkbox">
+            <label for="alert5" class="cr1"></label>
+          </li>
+          <li>
+            <div class="filter-list-card-controls">
+              <div class="filter-list-card-links">
+                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Public</a></span>
+                <span class="filter-list-card-link">Panel: <a href="">More Critical Thing</a></span>
+              </div>
+              <div class="filter-list-card-config">
+                <a href="#"><i class="fa fa-cog"></i></a>
+              </div>
+              <div class="filter-list-card-expand" ng-click="alert5.expanded = !alert5.expanded">
+                <i class="fa fa-angle-right" ng-show="!alert5.expanded"></i>
+                <i class="fa fa-angle-down" ng-show="alert5.expanded"></i>
+              </div>
+            </div>
+            <span class="filter-list-card-title">More Critical Thing</span>
+            <span class="filter-list-card-status">
+              <span class="filter-list-card-state online">Online</span> for 2 months
+            </span>
+          </li>
+        </ul>
+        <div class="filter-list-card-details" ng-show="alert5.expanded">
+          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
+          <div class="tight-form last">
+            <ul class="tight-form-list">
+              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
+              <li class="tight-form-item">apps</li>
+              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
+              <li class="tight-form-item">fakesite</li>
+              <li class="tight-form-item">counters</li>
+              <li class="tight-form-item">requests</li>
+              <li class="tight-form-item">count</li>
+              <li class="tight-form-item">scaleToSeconds(1)</li>
+              <li class="tight-form-item">aliasByNode(2)</li>
+            </ul>
+            <div class="clearfix"></div>
+          </div>
+        </div>
+      </li>
+    </ul>
+  </div>
+</div>

+ 3 - 4
public/app/features/dashboard/partials/shareModal.html

@@ -107,7 +107,7 @@
 </script>
 
 <script type="text/ng-template" id="shareSnapshot.html">
-	<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl">
+	<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
 		<div class="share-modal-big-icon">
 			<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
 			<i ng-if="!loading" class="gf-icon gf-icon-snap-multi"></i>
@@ -175,10 +175,9 @@
 				<i class="fa fa-save"></i>
 				Local Snapshot
 			</button>
-
-			<button class="btn btn-primary btn-large" ng-click="createSnapshot(true)" ng-disabled="loading">
+			<button class="btn btn-primary btn-large" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
 				<i class="fa fa-cloud-upload"></i>
-				Publish to snapshot.raintank.io
+				{{sharingButtonText}}
 			</button>
 		</div>
 

+ 10 - 1
public/app/features/dashboard/rowCtrl.js

@@ -42,12 +42,21 @@ function (angular, _, config) {
     };
 
     $scope.deleteRow = function() {
+      function delete_row() {
+        $scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
+      }
+
+      if (!$scope.row.panels.length) {
+        delete_row();
+        return;
+      }
+
       $scope.appEvent('confirm-modal', {
         title: 'Are you sure you want to delete this row?',
         icon: 'fa-trash',
         yesText: 'Delete',
         onConfirm: function() {
-          $scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
+          delete_row();
         }
       });
     };

+ 8 - 1
public/app/features/dashboard/shareSnapshotCtrl.js

@@ -29,7 +29,14 @@ function (angular, _) {
       {text: 'Public on the web', value: 3},
     ];
 
-    $scope.externalUrl = '//snapshots-origin.raintank.io';
+    $scope.init = function() {
+      backendSrv.get('/api/snapshot/shared-options').then(function(options) {
+        $scope.externalUrl = options['externalSnapshotURL'];
+        $scope.sharingButtonText = options['externalSnapshotName'];
+        $scope.externalEnabled = options['externalEnabled'];
+      });
+    };
+
     $scope.apiUrl = '/api/snapshots';
 
     $scope.createSnapshot = function(external) {

+ 1 - 3
public/app/features/dashboard/timepicker/timepicker.html

@@ -16,14 +16,12 @@
 
 			<span ng-show="ctrl.dashboard.refresh" class="text-warning">
 				&nbsp;
-				&nbsp;
-				<i class="fa fa-refresh"></i>
 				Refresh every {{ctrl.dashboard.refresh}}
 			</span>
 		</a>
 	</li>
 
-	<li class="grafana-menu-refresh" ng-show="!ctrl.dashboard.refresh">
+	<li class="grafana-menu-refresh">
 		<a ng-click="ctrl.timeSrv.refreshDashboard()">
 			<i class="fa fa-refresh"></i>
 		</a>

+ 4 - 0
public/app/features/org/all.js

@@ -6,4 +6,8 @@ define([
   './userInviteCtrl',
   './orgApiKeysCtrl',
   './orgDetailsCtrl',
+  './pluginsCtrl',
+  './pluginEditCtrl',
+  './plugin_srv',
+  './plugin_directive',
 ], function () {});

+ 52 - 41
public/app/features/org/partials/datasourceHttpConfig.html

@@ -1,44 +1,55 @@
 <br>
 <h5>Http settings</h5>
-<div class="tight-form">
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="width: 80px">
-			Url
-		</li>
-		<li>
-			<input type="text" class="tight-form-input input-xlarge" ng-model='current.url' placeholder="http://my.server.com:8080" ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
-		</li>
-		<li class="tight-form-item">
-			Access <tip>Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request</tip>
-	  </li>
-		<li>
-			<select class="input-medium tight-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
-		</li>
-	</ul>
-	<div class="clearfix"></div>
+<div class="tight-form-container">
+	<div class="tight-form">
+		<ul class="tight-form-list">
+			<li class="tight-form-item" style="width: 80px">
+				Url
+			</li>
+			<li>
+				<input type="text" class="tight-form-input input-xlarge" ng-model='current.url' placeholder="http://my.server.com:8080" ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
+			</li>
+			<li class="tight-form-item">
+				Access <tip>Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request</tip>
+			</li>
+			<li>
+				<select class="input-medium tight-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
+			</li>
+		</ul>
+		<div class="clearfix"></div>
+	</div>
+	<div class="tight-form">
+		<ul class="tight-form-list">
+			<li class="tight-form-item" style="width: 80px">
+				Http Auth
+			</li>
+			<li class="tight-form-item">
+				<editor-checkbox text="Basic Auth" model="current.basicAuth"></editor-checkbox>
+			</li>
+			<li class="tight-form-item">
+				<editor-checkbox text="With Credentials" model="current.withCredentials"></editor-checkbox>
+			</li>
+		</ul>
+		<div class="clearfix"></div>
+	</div>
+	<div class="tight-form" ng-if="current.basicAuth">
+		<ul class="tight-form-list">
+			<li class="tight-form-item" style="width: 80px">
+				<i class="fa fa-remove invisible"></i>
+			</li>
+			<li class="tight-form-item">
+				User
+			</li>
+			<li ng-if="current.basicAuth">
+				<input type="text" class="tight-form-input input-medium" style="width: 136px" ng-model='current.basicAuthUser' placeholder="user" required></input>
+			</li>
+			<li class="tight-form-item" style="width: 66px" ng-if="current.basicAuth">
+				Password
+			</li>
+			<li ng-if="current.basicAuth">
+				<input type="password" class="tight-form-input input-medium" ng-model='current.basicAuthPassword' placeholder="password" required></input>
+			</li>
+		</ul>
+		<div class="clearfix"></div>
+	</div>
 </div>
-<div class="tight-form last">
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="width: 80px">
-			Basic Auth
-		</li>
-		<li class="tight-form-item">
-			<editor-checkbox text="Enable" model="current.basicAuth"></editor-checkbox>
-		</li>
-		<li class="tight-form-item" ng-if="current.basicAuth">
-			User
-		</li>
-		<li ng-if="current.basicAuth">
-			<input type="text" class="tight-form-input input-medium" style="width: 139px" ng-model='current.basicAuthUser' placeholder="user" required></input>
-		</li>
-		<li class="tight-form-item" style="width: 67px" ng-if="current.basicAuth">
-			Password
-		</li>
-		<li ng-if="current.basicAuth">
-			<input type="password" class="tight-form-input input-medium" ng-model='current.basicAuthPassword' placeholder="password" required></input>
-		</li>
-	</ul>
-	<div class="clearfix"></div>
-</div>
-
-

+ 40 - 35
public/app/features/org/partials/datasources.html

@@ -5,47 +5,52 @@
 	</ul>
 </topnav>
 
-<div class="page-container">
-	<div class="page">
+<div class="page-container" style="background: transparent; border: 0;">
+	<div class="page-wide">
 		<h2>Data sources</h2>
 
 		<div ng-if="datasources.length === 0">
 			<em>No datasources defined</em>
 		</div>
 
-		<table class="grafana-options-table" ng-if="datasources.length > 0">
-			<tr>
-				<td><strong>Name</strong></td>
-				<td><strong>Url</strong></td>
-				<td></td>
-				<td></td>
-				<td></td>
-			</tr>
-			<tr ng-repeat="ds in datasources">
-				<td style="width:1%">
-					<i class="fa fa-database"></i> &nbsp;
-					{{ds.name}}
-				</td>
-				<td style="width:90%">
-					{{ds.url}}
-				</td>
-				<td style="width:2%" class="text-center">
-					<span ng-if="ds.isDefault">
-						<span class="label label-info">default</span>
-					</span>
-				</td>
-				<td style="width: 1%">
-					<a href="datasources/edit/{{ds.id}}" class="btn btn-inverse btn-mini">
-						<i class="fa fa-edit"></i>
-						Edit
-					</a>
-				</td>
-				<td style="width: 1%">
-					<a ng-click="remove(ds)" class="btn btn-danger btn-mini">
-						<i class="fa fa-remove"></i>
-					</a>
-				</td>
-			</tr>
+		<table class="filter-table" ng-if="datasources.length > 0">
+			<thead>
+				<tr>
+					<th><strong>Name</strong></th>
+					<th><strong>Url</strong></th>
+					<th style="width: 60px;"></th>
+					<th style="width: 65px;"></th>
+					<th style="width: 34px;"></th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr ng-repeat="ds in datasources">
+					<td>
+						<a href="datasources/edit/{{ds.id}}">
+							<i class="fa fa-database"></i> &nbsp; {{ds.name}}
+						</a>
+					</td>
+					<td>
+						<span class="ellipsis">{{ds.url}}</span>
+					</td>
+					<td class="text-center">
+						<span ng-if="ds.isDefault">
+							<span class="label label-info">default</span>
+						</span>
+					</td>
+					<td class="text-right">
+						<a href="datasources/edit/{{ds.id}}" class="btn btn-inverse btn-mini">
+							<i class="fa fa-edit"></i>
+							Edit
+						</a>
+					</td>
+					<td class="text-right">
+						<a ng-click="remove(ds)" class="btn btn-danger btn-mini">
+							<i class="fa fa-remove"></i>
+						</a>
+					</td>
+				</tr>
+			</tbody>
 		</table>
 
 	</div>

+ 21 - 18
public/app/features/org/partials/orgApiKeys.html

@@ -4,9 +4,8 @@
 	</ul>
 </topnav>
 
-<div class="page-container">
-	<div class="page">
-
+<div class="page-container" style="background: transparent; border: 0;">
+	<div class="page-wide">
 		<h2>
 			API Keys
 		</h2>
@@ -32,21 +31,25 @@
 			</ul>
 		</form>
 
-		<table class="grafana-options-table" style="width: 250px">
-			<tr>
-				<th style="text-align: left">Name</th>
-				<th style="text-align: left">Role</th>
-				<th></th>
-			</tr>
-			<tr ng-repeat="t in tokens">
-				<td>{{t.name}}</td>
-				<td>{{t.role}}</td>
-				<td style="width: 1%">
-					<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
-						<i class="fa fa-remove"></i>
-					</a>
-				</td>
-			</tr>
+		<table class="filter-table">
+			<thead>
+				<tr>
+					<th>Name</th>
+					<th>Role</th>
+					<th style="width: 34px;"></th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr ng-repeat="t in tokens">
+					<td>{{t.name}}</td>
+					<td>{{t.role}}</td>
+					<td>
+						<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
+							<i class="fa fa-remove"></i>
+						</a>
+					</td>
+				</tr>
+			</tbody>
 		</table>
 	</div>
 

+ 51 - 39
public/app/features/org/partials/orgUsers.html

@@ -4,8 +4,8 @@
 	</ul>
 </topnav>
 
-<div class="page-container">
-	<div class="page">
+<div class="page-container" style="background: transparent; border: 0;">
+	<div class="page-wide">
 
 		<h2>Organization users</h2>
 
@@ -18,21 +18,23 @@
 
 		<tabset>
 			<tab heading="Users ({{users.length}})">
-				<table class="grafana-options-table form-inline">
-					<tr>
-						<th>Login</th>
-						<th>Email</th>
-						<th>Role</th>
-						<th></th>
-					</tr>
+				<table class="filter-table form-inline">
+					<thead>
+						<tr>
+							<th>Login</th>
+							<th>Email</th>
+							<th>Role</th>
+							<th style="width: 34px;"></th>
+						</tr>
+					</thead>
 					<tr ng-repeat="user in users">
 						<td>{{user.login}}</td>
-						<td>{{user.email}}</td>
+						<td><span class="ellipsis">{{user.email}}</span></td>
 						<td>
 							<select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(user)">
 							</select>
 						</td>
-						<td style="width: 1%">
+						<td>
 							<a ng-click="removeUser(user)" class="btn btn-danger btn-mini">
 								<i class="fa fa-remove"></i>
 							</a>
@@ -41,36 +43,46 @@
 				</table>
 			</tab>
 			<tab heading="Pending Invitations ({{pendingInvites.length}})">
-				<div class="grafana-list-item" ng-repeat="invite in pendingInvites" ng-click="invite.expanded = !invite.expanded">
-					{{invite.email}}
-					<span ng-show="invite.name" style="padding-left: 20px"> {{invite.name}}</span>
-					<span class="pull-right">
-						<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)">
-							<i class="fa fa-clipboard"></i> Copy Invite
-						</button>
-						&nbsp;
-						<a class="pointer">
-							<i ng-show="!invite.expanded" class="fa fa-caret-right"></i>
-							<i ng-show="invite.expanded" class="fa fa-caret-down"></i>
-						</a>
-					</span>
-					<div ng-show="invite.expanded">
-						<a href="{{invite.url}}">{{invite.url}}</a><br>
-						<button class="btn btn-inverse btn-mini">
-							<i class="fa fa-envelope-o"></i> Resend invite
-						</button>
-						&nbsp;
-						<button class="btn btn-inverse btn-mini" ng-click="revokeInvite(invite, $event)">
-							<i class="fa fa-remove" style="color: red"></i> Revoke invite
-						</button>
-						<span style="padding-left: 15px">
-							Invited: <em> {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} </em>
-						</span>
-					<div>
-				</div>
+				<table class="filter-table form-inline">
+					<thead>
+						<tr>
+							<th>Email</th>
+							<th>Name</th>
+							<th></th>
+						</tr>
+					</thead>
+					<tbody ng-repeat="invite in pendingInvites">
+						<tr ng-click="invite.expanded = !invite.expanded" ng-class="{'expanded': invite.expanded}">
+							<td>{{invite.email}}</td>
+							<td>{{invite.name}}</td>
+							<td class="text-right">
+								<button class="btn btn-inverse btn-mini " data-clipboard-text="{{invite.url}}" clipboard-button ng-click="copyInviteToClipboard($event)">
+									<i class="fa fa-clipboard"></i> Copy Invite
+								</button>
+								&nbsp;
+								<button class="btn btn-inverse btn-mini">
+									Details
+									<i ng-show="!invite.expanded" class="fa fa-caret-right"></i>
+									<i ng-show="invite.expanded" class="fa fa-caret-down"></i>
+								</button>
+							</td>
+						</tr>
+						<tr ng-show="invite.expanded">
+							<td colspan="3">
+								<a href="{{invite.url}}">{{invite.url}}</a><br><br>
+								&nbsp;
+								<button class="btn btn-inverse btn-mini" ng-click="revokeInvite(invite, $event)">
+									<i class="fa fa-remove" style="color: red"></i> Revoke invite
+								</button>
+								<span style="padding-left: 15px">
+									Invited: <em> {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} </em>
+								</span>
+							</td>
+						</tr>
+					</tbody>
+				</table>
 			</tab>
 		</tabset>
-
 	</div>
 </div>
 

+ 3 - 0
public/app/features/org/partials/pluginConfigCore.html

@@ -0,0 +1,3 @@
+<div>
+{{current.type}} plugin does not have any additional config.
+</div>

+ 42 - 0
public/app/features/org/partials/pluginEdit.html

@@ -0,0 +1,42 @@
+<topnav title="Plugins" icon="fa fa-fw fa-cubes" subnav="true">
+	<ul class="nav">
+		<li ><a href="plugins">Overview</a></li>
+		<li class="active" ><a href="plugins/edit/{{current.type}}">Edit</a></li>
+	</ul>
+</topnav>
+
+<div class="page-container">
+	<div class="page">
+		<h2>Edit Plugin</h2>
+
+
+		<form name="editForm">
+			<div class="tight-form">
+				<ul class="tight-form-list">
+					<li class="tight-form-item" style="width: 80px">
+						Type
+					</li>
+					<li>
+						<li>
+							<input type="text" disabled="disabled" class="input-xlarge tight-form-input" ng-model="current.type">
+						</li>
+					</li>
+					<li class="tight-form-item">
+						Default&nbsp;
+						<input class="cr1" id="current.enabled" type="checkbox" ng-model="current.enabled" ng-checked="current.enabled">
+						<label for="current.enabled" class="cr1"></label>
+					</li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+			<br>
+			<plugin-config-loader plugin="current"></plugin-config-loader>
+			<div class="pull-right" style="margin-top: 35px">
+				<button type="submit" class="btn btn-success" ng-click="update()">Save</button>
+				<a class="btn btn-inverse" href="plugins">Cancel</a>
+			</div>
+			<br>
+		</form>
+
+	</div>
+</div>

+ 41 - 0
public/app/features/org/partials/plugins.html

@@ -0,0 +1,41 @@
+<topnav title="Plugins" icon="fa fa-fw fa-cubes" subnav="true">
+	<ul class="nav">
+		<li class="active" ><a href="plugins">Overview</a></li>
+	</ul>
+</topnav>
+
+<div class="page-container">
+	<div class="page">
+		<h2>Plugins</h2>
+
+		<div ng-if="!plugins">
+			<em>No plugins defined</em>
+		</div>
+
+		<table class="grafana-options-table" ng-if="plugins">
+			<tr>
+				<td><strong>Type</strong></td>
+				<td></td>
+				<td></td>
+			</tr>
+			<tr ng-repeat="(type, p) in plugins">
+				<td style="width:1%">
+					<i class="fa fa-cubes"></i> &nbsp;
+					{{p.type}}
+				</td>
+				<td style="width: 1%">
+					<a href="plugins/edit/{{p.type}}" class="btn btn-inverse btn-mini">
+						<i class="fa fa-edit"></i>
+						Edit
+					</a>
+				</td>
+				<td style="width: 1%">
+					Enabled&nbsp;
+					<input  id="p.enabled" type="checkbox" ng-model="p.enabled" ng-checked="p.enabled" ng-change="update(p)">
+					<label for="p.enabled"></label>
+				</td>
+			</tr>
+		</table>
+
+	</div>
+</div>

+ 35 - 0
public/app/features/org/pluginEditCtrl.js

@@ -0,0 +1,35 @@
+define([
+  'angular',
+  'lodash',
+  'app/core/config',
+],
+function (angular, _, config) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('PluginEditCtrl', function($scope, pluginSrv, $routeParams) {
+    $scope.init = function() {
+      $scope.current = {};
+      $scope.getPlugins();
+    };
+
+    $scope.getPlugins = function() {
+      pluginSrv.get($routeParams.type).then(function(result) {
+        $scope.current = _.clone(result);
+      });
+    };
+
+    $scope.update = function() {
+      $scope._update();
+    };
+
+    $scope._update = function() {
+      pluginSrv.update($scope.current).then(function() {
+        window.location.href = config.appSubUrl + "plugins";
+      });
+    };
+
+    $scope.init();
+  });
+});

+ 47 - 0
public/app/features/org/plugin_directive.js

@@ -0,0 +1,47 @@
+define([
+  'angular',
+],
+function (angular) {
+  'use strict';
+
+  var module = angular.module('grafana.directives');
+
+  module.directive('pluginConfigLoader', function($compile) {
+    return {
+      restrict: 'E',
+      link: function(scope, elem) {
+        var directive = 'grafana-plugin-core';
+        //wait for the parent scope to be applied.
+        scope.$watch("current", function(newVal) {
+          if (newVal) {
+            if (newVal.module) {
+              directive = 'grafana-plugin-'+newVal.type;
+            }
+            scope.require([newVal.module], function () {
+              var panelEl = angular.element(document.createElement(directive));
+              elem.append(panelEl);
+              $compile(panelEl)(scope);
+            });
+          }
+        });
+      }
+    };
+  });
+
+  module.directive('grafanaPluginCore', function() {
+    return {
+      restrict: 'E',
+      templateUrl: 'app/features/org/partials/pluginConfigCore.html',
+      transclude: true,
+      link: function(scope) {
+        scope.update = function() {
+          //Perform custom save events to the plugins own backend if needed.
+
+          // call parent update to commit the change to the plugin object.
+          // this will cause the page to reload.
+          scope._update();
+        };
+      }
+    };
+  });
+});

+ 58 - 0
public/app/features/org/plugin_srv.js

@@ -0,0 +1,58 @@
+define([
+  'angular',
+  'lodash',
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.service('pluginSrv', function($rootScope, $timeout, $q, backendSrv) {
+    var self = this;
+    this.init = function() {
+      console.log("pluginSrv init");
+      this.plugins = {};
+    };
+
+    this.get = function(type) {
+      return $q(function(resolve) {
+        if (type in self.plugins) {
+          return resolve(self.plugins[type]);
+        }
+        backendSrv.get('/api/plugins').then(function(results) {
+          _.forEach(results, function(p) {
+            self.plugins[p.type] = p;
+          });
+          return resolve(self.plugins[type]);
+        });
+      });
+    };
+
+    this.getAll = function() {
+      return $q(function(resolve) {
+        if (!_.isEmpty(self.plugins)) {
+          return resolve(self.plugins);
+        }
+        backendSrv.get('api/plugins').then(function(results) {
+          _.forEach(results, function(p) {
+            self.plugins[p.type] = p;
+          });
+          return resolve(self.plugins);
+        });
+      });
+    };
+
+    this.update = function(plugin) {
+      return $q(function(resolve, reject) {
+        backendSrv.post('/api/plugins', plugin).then(function(resp) {
+          self.plugins[plugin.type] = plugin;
+          resolve(resp);
+        }, function(resp) {
+          reject(resp);
+        });
+      });
+    };
+
+    this.init();
+  });
+});

+ 33 - 0
public/app/features/org/pluginsCtrl.js

@@ -0,0 +1,33 @@
+define([
+  'angular',
+  'app/core/config',
+],
+function (angular, config) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('PluginsCtrl', function($scope, $location, pluginSrv) {
+
+    $scope.init = function() {
+      $scope.plugins = {};
+      $scope.getPlugins();
+    };
+
+    $scope.getPlugins = function() {
+      pluginSrv.getAll().then(function(result) {
+        console.log(result);
+        $scope.plugins = result;
+      });
+    };
+
+    $scope.update = function(plugin) {
+      pluginSrv.update(plugin).then(function() {
+        window.location.href = config.appSubUrl + $location.path();
+      });
+    };
+
+    $scope.init();
+
+  });
+});

+ 2 - 2
public/app/features/panel/panel_directive.js

@@ -13,9 +13,9 @@ function (angular, $, config) {
       restrict: 'E',
       link: function(scope, elem, attr) {
         var getter = $parse(attr.type), panelType = getter(scope);
-        var panelPath = config.panels[panelType].path;
+        var module = config.panels[panelType].module;
 
-        scope.require([panelPath + "/module"], function () {
+        scope.require([module], function () {
           var panelEl = angular.element(document.createElement('grafana-panel-' + panelType));
           elem.append(panelEl);
           $compile(panelEl)(scope);

+ 8 - 2
public/app/features/panel/panel_helper.js

@@ -11,7 +11,7 @@ function (angular, _, $, kbn, dateMath, rangeUtil) {
 
   var module = angular.module('grafana.services');
 
-  module.service('panelHelper', function(timeSrv, $rootScope) {
+  module.service('panelHelper', function(timeSrv, $rootScope, $q) {
     var self = this;
 
     this.setTimeQueryStart = function(scope) {
@@ -59,7 +59,9 @@ function (angular, _, $, kbn, dateMath, rangeUtil) {
         scope.resolution = Math.ceil($(window).width() * (scope.panel.span / 12));
       }
 
-      scope.interval = kbn.calculateInterval(scope.range, scope.resolution, scope.panel.interval);
+      var panelInterval = scope.panel.interval;
+      var datasourceInterval = (scope.datasource || {}).interval;
+      scope.interval = kbn.calculateInterval(scope.range, scope.resolution, panelInterval || datasourceInterval);
     };
 
     this.applyPanelTimeOverrides = function(scope) {
@@ -103,6 +105,10 @@ function (angular, _, $, kbn, dateMath, rangeUtil) {
     };
 
     this.issueMetricQuery = function(scope, datasource) {
+      if (!scope.panel.targets || scope.panel.targets.length === 0) {
+        return $q.when([]);
+      }
+
       var metricsQuery = {
         range: scope.range,
         rangeRaw: scope.rangeRaw,

+ 24 - 15
public/app/features/profile/partials/profile.html

@@ -5,8 +5,8 @@
 	</ul>
 </topnav>
 
-<div class="page-container">
-	<div class="page">
+<div class="page-container" style="background: transparent; border: 0;">
+	<div class="page-wide">
 
 		<h2>Profile</h2>
 
@@ -62,19 +62,28 @@
 
 		<h3>Organizations</h3>
 
-		<table class="grafana-options-table">
-			<tr ng-repeat="org in orgs">
-				<td style="width: 98%"><strong>Name: </strong> {{org.name}}</td>
-				<td><strong>Role: </strong> {{org.role}}</td>
-				<td class="nobg max-width-btns">
-					<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
-						Current
-					</span>
-					<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
-						Select
-					</a>
-				</td>
-			</tr>
+		<table class="filter-table form-inline">
+			<thead>
+				<tr>
+					<th>Name</th>
+					<th>Role</th>
+					<th></th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr ng-repeat="org in orgs">
+					<td>{{org.name}}</td>
+					<td>{{org.role}}</td>
+					<td class="text-right">
+						<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
+							Current
+						</span>
+						<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
+							Select
+						</a>
+					</td>
+				</tr>
+			</tbody>
 		</table>
 
 	</div>

+ 1 - 1
public/app/features/templating/partials/editor.html

@@ -129,7 +129,7 @@
 									<editor-checkbox text="Include auto interval" model="current.auto" change="runQuery()"></editor-checkbox>
 								</li>
 								<li class="tight-form-item" ng-show="current.auto">
-									Auto interval steps <tip>How many steps, roughly, the interval is rounded and will not always match this count<tip>
+									Auto interval steps <tip>How many times should the current time range be divided to calculate the value</tip>
 								</li>
 								<li>
 									<select class="input-mini tight-form-input last" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>

+ 1 - 0
public/app/features/templating/templateSrv.js

@@ -79,6 +79,7 @@ function (angular, _) {
     this.highlightVariablesAsHtml = function(str) {
       if (!str || !_.isString(str)) { return str; }
 
+      str = _.escape(str);
       this._regex.lastIndex = 0;
       return str.replace(this._regex, function(match, g1, g2) {
         if (self._values[g1 || g2]) {

Некоторые файлы не были показаны из-за большого количества измененных файлов