Преглед изворни кода

Merge branch 'master' into dash-edit-mode

Conflicts:
	public/app/features/dashboard/partials/settings.html
Torkel Ödegaard пре 9 година
родитељ
комит
2546ec062b
100 измењених фајлова са 1878 додато и 1263 уклоњено
  1. 26 0
      CHANGELOG.md
  2. 17 0
      Makefile
  3. 1 3
      README.md
  4. 1 1
      build.go
  5. 2 2
      circle.yml
  6. 1 1
      docs/sources/http_api/dashboard.md
  7. 3 3
      docs/sources/installation/debian.md
  8. 4 4
      docs/sources/installation/rpm.md
  9. 1 1
      docs/sources/plugins/index.md
  10. 2 1
      package.json
  11. 2 2
      pkg/api/cloudwatch/metrics.go
  12. 4 0
      pkg/api/dashboard_snapshot.go
  13. 0 1
      pkg/api/dataproxy.go
  14. 1 1
      pkg/api/dtos/plugins.go
  15. 8 1
      pkg/api/index.go
  16. 6 5
      pkg/api/plugins.go
  17. 2 1
      pkg/cmd/grafana-cli/commands/commands.go
  18. 12 7
      pkg/cmd/grafana-cli/commands/install_command.go
  19. 3 8
      pkg/cmd/grafana-cli/commands/remove_command.go
  20. 5 5
      pkg/cmd/grafana-cli/main.go
  21. 1 1
      pkg/models/dashboard_snapshot.go
  22. 22 0
      pkg/models/org_user.go
  23. 7 5
      pkg/plugins/dashboard_importer.go
  24. 15 9
      pkg/plugins/models.go
  25. 1 1
      pkg/services/sqlstore/dashboard.go
  26. 0 1
      pkg/services/sqlstore/plugin_setting.go
  27. 3 0
      public/app/core/components/dashboard_selector.ts
  28. 12 8
      public/app/core/components/info_popover.ts
  29. 6 17
      public/app/core/components/switch.ts
  30. 2 2
      public/app/core/directives/dropdown_typeahead.js
  31. 7 4
      public/app/core/directives/metric_segment.js
  32. 8 2
      public/app/core/directives/plugin_component.ts
  33. 0 2
      public/app/core/services/context_srv.ts
  34. 56 29
      public/app/core/services/datasource_srv.js
  35. 1 1
      public/app/core/services/segment_srv.js
  36. 2 0
      public/app/core/table_model.ts
  37. 1 1
      public/app/core/utils/emitter.ts
  38. 7 4
      public/app/features/dashboard/partials/settings.html
  39. 1 1
      public/app/features/dashboard/timepicker/timepicker.html
  40. 1 1
      public/app/features/dashboard/timepicker/timepicker.ts
  41. 1 1
      public/app/features/dashboard/viewStateSrv.js
  42. 3 4
      public/app/features/org/prefs_control.ts
  43. 1 0
      public/app/features/panel/all.js
  44. 110 0
      public/app/features/panel/metrics_ds_selector.ts
  45. 2 12
      public/app/features/panel/metrics_panel_ctrl.ts
  46. 2 2
      public/app/features/panel/panel_ctrl.ts
  47. 60 1
      public/app/features/panel/partials/query_editor_row.html
  48. 0 34
      public/app/features/panel/query_ctrl.ts
  49. 100 2
      public/app/features/panel/query_editor_row.ts
  50. 4 4
      public/app/features/plugins/ds_edit_ctrl.ts
  51. 4 4
      public/app/features/plugins/import_list/import_list.html
  52. 2 2
      public/app/features/plugins/import_list/import_list.ts
  53. 7 7
      public/app/features/plugins/partials/ds_edit.html
  54. 30 25
      public/app/features/plugins/partials/ds_http_settings.html
  55. 2 2
      public/app/features/plugins/partials/update_instructions.html
  56. 20 0
      public/app/features/templating/editorCtrl.js
  57. 96 59
      public/app/features/templating/partials/editor.html
  58. 5 1
      public/app/features/templating/templateSrv.js
  59. 41 10
      public/app/features/templating/templateValuesSrv.js
  60. 14 51
      public/app/partials/metrics.html
  61. 17 12
      public/app/plugins/datasource/cloudwatch/partials/config.html
  62. 2 2
      public/app/plugins/datasource/cloudwatch/partials/query.editor.html
  63. 51 50
      public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
  64. 11 0
      public/app/plugins/datasource/elasticsearch/bucket_agg.js
  65. BIN
      public/app/plugins/datasource/elasticsearch/img/logo_large.png
  66. 69 104
      public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html
  67. 61 117
      public/app/plugins/datasource/elasticsearch/partials/metric_agg.html
  68. 28 29
      public/app/plugins/datasource/elasticsearch/partials/query.editor.html
  69. 4 0
      public/app/plugins/datasource/elasticsearch/query_builder.js
  70. 43 0
      public/app/plugins/datasource/elasticsearch/query_ctrl.ts
  71. 3 2
      public/app/plugins/datasource/elasticsearch/query_def.js
  72. 6 4
      public/app/plugins/datasource/grafana/partials/query.editor.html
  73. 2 2
      public/app/plugins/datasource/graphite/add_graphite_func.js
  74. 7 0
      public/app/plugins/datasource/graphite/gfunc.js
  75. 21 15
      public/app/plugins/datasource/graphite/partials/query.editor.html
  76. 2 2
      public/app/plugins/datasource/influxdb/README.md
  77. 8 8
      public/app/plugins/datasource/influxdb/influx_query.ts
  78. 86 63
      public/app/plugins/datasource/influxdb/partials/query.editor.html
  79. 1 1
      public/app/plugins/datasource/influxdb/partials/query.options.html
  80. 1 1
      public/app/plugins/datasource/influxdb/partials/query_part.html
  81. 2 2
      public/app/plugins/datasource/influxdb/query_builder.js
  82. 10 1
      public/app/plugins/datasource/influxdb/query_ctrl.ts
  83. 21 9
      public/app/plugins/datasource/influxdb/response_parser.ts
  84. 17 9
      public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts
  85. 250 220
      public/app/plugins/datasource/opentsdb/partials/query.editor.html
  86. 48 74
      public/app/plugins/datasource/prometheus/partials/query.editor.html
  87. 11 12
      public/app/plugins/panel/dashlist/module.ts
  88. 1 1
      public/app/plugins/panel/graph/graph.js
  89. 83 83
      public/app/plugins/panel/graph/module.ts
  90. 1 1
      public/app/plugins/panel/pluginlist/module.html
  91. 5 5
      public/app/plugins/panel/pluginlist/module.ts
  92. 49 0
      public/app/plugins/panel/singlestat/editor.html
  93. 152 35
      public/app/plugins/panel/singlestat/module.ts
  94. 33 28
      public/app/plugins/panel/table/module.ts
  95. 1 1
      public/app/plugins/panel/table/renderer.ts
  96. 6 8
      public/app/plugins/panel/text/module.ts
  97. 2 1
      public/app/system.conf.js
  98. 1 0
      public/sass/_grafana.scss
  99. 3 3
      public/sass/_variables.dark.scss
  100. 1 1
      public/sass/_variables.light.scss

+ 26 - 0
CHANGELOG.md

@@ -1,3 +1,29 @@
+# 3.0.0-beta6 (unreleased)
+
+### Enhancements
+* **Singlestat**: Support for gauges in singlestat panel. closes [#3688](https://github.com/grafana/grafana/pull/3688)
+* **Templating**: Support for data source as variable, closes [#816](https://github.com/grafana/grafana/pull/816)
+
+### Bug fixes
+* **InfluxDB 0.12**: Fixed issue templating and `show tag values` query only returning tags for first measurement,  fixes [#4726](https://github.com/grafana/grafana/issues/4726)
+* **Templating**: Fixed issue with regex formating when matching multiple values, fixes [#4755](https://github.com/grafana/grafana/issues/4755)
+* **Templating**: Fixed issue with custom all value and escaping, fixes [#4736](https://github.com/grafana/grafana/issues/4736)
+* **Dashlist**: Fixed issue dashboard list panel and caching tags, fixes [#4768](https://github.com/grafana/grafana/issues/4768)
+* **Graph**: Fixed issue with unneeded scrollbar in legend for Firefox, fixes [#4760](https://github.com/grafana/grafana/issues/4760)
+* **Table panel**: Fixed issue table panel formating string array properties, fixes [#4791](https://github.com/grafana/grafana/issues/4791)
+* **grafana-cli**: Improve error message when failing to install plugins due to corrupt response, fixes [#4651](https://github.com/grafana/grafana/issues/4651)
+* **Singlestat**: Fixes prefix an postfix for gauges, fixes [#4812](https://github.com/grafana/grafana/issues/4812)
+* **Singlestat**: Fixes auto-refresh on change for some options, fixes [#4809](https://github.com/grafana/grafana/issues/4809)
+
+### Breaking changes
+**Data Source Query Editors**: Issue [#3900](https://github.com/grafana/grafana/issues/3900)
+
+Query editors have been updated to use the new form styles. External data source plugins needs to be
+updated to work. Sorry to introduce breaking change this late in beta phase. We wanted to get this change
+in before 3.0 stable is released so we don't have to break data sources in next release (3.1). If you are
+a data source plugin author and want help for how the new form styles work please ask for help in
+slack channel (link to slack channel in readme).
+
 # 3.0.0-beta5 (2016-04-15)
 
 ### Bug fixes

+ 17 - 0
Makefile

@@ -0,0 +1,17 @@
+all: deps build
+
+deps:
+	go run build.go setup
+	godep restore
+	npm install
+
+build:
+	go run build.go build
+	npm run build
+
+test:
+	godep go test -v ./pkg/...
+	npm test
+
+run:
+	./bin/grafana-server

+ 1 - 3
README.md

@@ -103,8 +103,7 @@ npm (v2.5.0) and grunt (v0.4.5). Run the following:
 
 ```bash
 npm install
-npm install -g grunt-cli
-grunt
+npm run build
 ```
 
 ### Recompile backend on source change
@@ -145,4 +144,3 @@ please [sign the CLA](http://docs.grafana.org/project/cla/)
 
 Grafana is distributed under Apache 2.0 License.
 Work in progress Grafana 2.0 (with included Grafana backend)
-

+ 1 - 1
build.go

@@ -306,7 +306,7 @@ func ChangeWorkingDir(dir string) {
 }
 
 func grunt(params ...string) {
-	runPrint("./node_modules/grunt-cli/bin/grunt", params...)
+	runPrint("./node_modules/.bin/grunt", params...)
 }
 
 func setup() {

+ 2 - 2
circle.yml

@@ -25,12 +25,12 @@ test:
      # Go test
      - godep go test -v ./pkg/...
      # js tests
-     - ./node_modules/grunt-cli/bin/grunt test
+     - npm test
      - npm run coveralls
 
 deployment:
   master:
     branch: master
     owner: grafana
-    commands: 
+    commands:
       - ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}

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

@@ -191,7 +191,7 @@ Will return the home dashboard.
 
 `GET /api/dashboards/tags`
 
-Get all tabs of dashboards
+Get all tags of dashboards
 
 **Example Request**:
 

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

@@ -11,7 +11,7 @@ page_keywords: grafana, installation, debian, ubuntu, guide
 Description | Download
 ------------ | -------------
 Stable .deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb)
-Beta .deb for Debian-based Linux |   [grafana_3.0.0-beta51460725904_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta51460725904_amd64.deb)
+Beta .deb for Debian-based Linux |   [grafana_3.0.0-beta61461918338_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta61461918338_amd64.deb)
 
 ## Install Stable
 
@@ -21,9 +21,9 @@ Beta .deb for Debian-based Linux |   [grafana_3.0.0-beta51460725904_amd64.deb](h
 
 ## Install 3.0 Beta
 
-    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta51460725904_amd64.deb
+    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta61461918338_amd64.deb
     $ sudo apt-get install -y adduser libfontconfig
-    $ sudo dpkg -i grafana_3.0.0-beta51460725904_amd64.deb
+    $ sudo dpkg -i grafana_3.0.0-beta61461918338_amd64.deb
 
 ## APT Repository
 

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

@@ -11,7 +11,7 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
 Description | Download
 ------------ | -------------
 Stable .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)
-Beta .RPM for CentOS / Fedor / OpenSuse / Redhat Linux | [grafana-3.0.0-beta51460725904.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta51460725904§.x86_64.rpm)
+Beta .RPM for CentOS / Fedor / OpenSuse / Redhat Linux | [grafana-3.0.0-beta61461918338.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta61461918338§.x86_64.rpm)
 
 ## Install Stable Release from package file
 
@@ -34,18 +34,18 @@ Or install manually using `rpm`.
 
 You can install Grafana using Yum directly.
 
-    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta51460725904.x86_64.rpm
+    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta61461918338.x86_64.rpm
 
 Or install manually using `rpm`.
 
 #### On CentOS / Fedora / Redhat:
 
     $ sudo yum install initscripts fontconfig
-    $ sudo rpm -Uvh grafana-3.0.0-beta51460725904.x86_64.rpm
+    $ sudo rpm -Uvh grafana-3.0.0-beta61461918338.x86_64.rpm
 
 #### On OpenSuse:
 
-    $ sudo rpm -i --nodeps grafana-3.0.0-beta51460725904.x86_64.rpm
+    $ sudo rpm -i --nodeps grafana-3.0.0-beta61461918338.x86_64.rpm
 
 
 ## Install via YUM Repository

+ 1 - 1
docs/sources/plugins/index.md

@@ -15,7 +15,7 @@ Grafana already have a strong community of contributors and plugin developers.
 By making it easier to develop and install plugins we hope that the community
 can grow even stronger and develop new plugins that we would never think about.
 
-You can discover available plugins on [Grafana.net](http://grafana.net)
+You can discover available plugins on [Grafana.net](https://grafana.net)
 
 
 

+ 2 - 1
package.json

@@ -54,7 +54,7 @@
     "phantomjs-prebuilt": "^2.1.3",
     "reflect-metadata": "0.1.2",
     "rxjs": "5.0.0-beta.4",
-    "sass-lint": "^1.5.0",
+    "sass-lint": "^1.6.0",
     "systemjs": "0.19.24"
   },
   "engines": {
@@ -62,6 +62,7 @@
     "npm": "2.14.x"
   },
   "scripts": {
+    "build": "grunt",
     "test": "grunt test",
     "coveralls": "grunt karma:coveralls && rm -rf ./coverage"
   },

+ 2 - 2
pkg/api/cloudwatch/metrics.go

@@ -56,7 +56,7 @@ func init() {
 			"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/Events":   {"Invocations", "FailedInvocations", "TriggeredRules", "MatchedEvents", "ThrottledRules"},
-		"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/Kinesis":  {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"},
 		"AWS/Lambda":   {"Invocations", "Errors", "Duration", "Throttles"},
 		"AWS/Logs":     {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
 		"AWS/ML":       {"PredictCount", "PredictFailureCount"},
@@ -88,7 +88,7 @@ func init() {
 		"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
 		"AWS/ES":               {},
 		"AWS/Events":           {"RuleName"},
-		"AWS/Kinesis":          {"StreamName"},
+		"AWS/Kinesis":          {"StreamName", "ShardID"},
 		"AWS/Lambda":           {"FunctionName"},
 		"AWS/Logs":             {"LogGroupName", "DestinationType", "FilterName"},
 		"AWS/ML":               {"MLModelId", "RequestMode"},

+ 4 - 0
pkg/api/dashboard_snapshot.go

@@ -21,6 +21,10 @@ func GetSharingOptions(c *middleware.Context) {
 }
 
 func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
+	if cmd.Name == "" {
+		cmd.Name = "Unnamed snapshot"
+	}
+
 	if cmd.External {
 		// external snapshot ref requires key and delete key
 		if cmd.Key == "" || cmd.DeleteKey == "" {

+ 0 - 1
pkg/api/dataproxy.go

@@ -41,7 +41,6 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *ht
 			req.URL.RawQuery = reqQueryVals.Encode()
 		} else if ds.Type == m.DS_INFLUXDB {
 			req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
-			reqQueryVals.Add("db", ds.Database)
 			req.URL.RawQuery = reqQueryVals.Encode()
 			if !ds.BasicAuth {
 				req.Header.Del("Authorization")

+ 1 - 1
pkg/api/dtos/plugins.go

@@ -48,6 +48,6 @@ func (slice PluginList) Swap(i, j int) {
 type ImportDashboardCommand struct {
 	PluginId  string                         `json:"pluginId"`
 	Path      string                         `json:"path"`
-	Reinstall bool                           `json:"reinstall"`
+	Overwrite bool                           `json:"overwrite"`
 	Inputs    []plugins.ImportDashboardInput `json:"inputs"`
 }

+ 8 - 1
pkg/api/index.go

@@ -103,6 +103,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 			}
 
 			for _, include := range plugin.Includes {
+				if !c.HasUserRole(include.Role) {
+					continue
+				}
+
 				if include.Type == "page" && include.AddToNav {
 					link := &dtos.NavLink{
 						Url:  setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + include.Slug,
@@ -110,6 +114,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 					}
 					appLink.Children = append(appLink.Children, link)
 				}
+
 				if include.Type == "dashboard" && include.AddToNav {
 					link := &dtos.NavLink{
 						Url:  setting.AppSubUrl + "/dashboard/db/" + include.Slug,
@@ -124,7 +129,9 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 				appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
 			}
 
-			data.MainNavLinks = append(data.MainNavLinks, appLink)
+			if len(appLink.Children) > 0 {
+				data.MainNavLinks = append(data.MainNavLinks, appLink)
+			}
 		}
 	}
 

+ 6 - 5
pkg/api/plugins.go

@@ -156,11 +156,12 @@ func GetPluginReadme(c *middleware.Context) Response {
 func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand) Response {
 
 	cmd := plugins.ImportDashboardCommand{
-		OrgId:    c.OrgId,
-		UserId:   c.UserId,
-		PluginId: apiCmd.PluginId,
-		Path:     apiCmd.Path,
-		Inputs:   apiCmd.Inputs,
+		OrgId:     c.OrgId,
+		UserId:    c.UserId,
+		PluginId:  apiCmd.PluginId,
+		Path:      apiCmd.Path,
+		Inputs:    apiCmd.Inputs,
+		Overwrite: apiCmd.Overwrite,
 	}
 
 	if err := bus.Dispatch(&cmd); err != nil {

+ 2 - 1
pkg/cmd/grafana-cli/commands/commands.go

@@ -4,6 +4,7 @@ import (
 	"os"
 
 	"github.com/codegangsta/cli"
+	"github.com/fatih/color"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
 )
 
@@ -12,7 +13,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C
 
 		cmd := &contextCommandLine{context}
 		if err := command(cmd); err != nil {
-			log.Error("\nError: ")
+			log.Errorf("\n%s: ", color.RedString("Error"))
 			log.Errorf("%s\n\n", err)
 
 			cmd.ShowHelp()

+ 12 - 7
pkg/cmd/grafana-cli/commands/install_command.go

@@ -126,11 +126,16 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 	defer func() {
 		if r := recover(); r != nil {
 			retryCount++
-			if retryCount == 1 {
-				log.Debug("\nFailed downloading. Will retry once.\n")
-				downloadFile(pluginName, filePath, url)
+			if retryCount < 3 {
+				fmt.Println("Failed downloading. Will retry once.")
+				err = downloadFile(pluginName, filePath, url)
 			} else {
-				panic(r)
+				failure := fmt.Sprintf("%v", r)
+				if failure == "runtime error: makeslice: len out of range" {
+					err = fmt.Errorf("Corrupt http response from source. Please try again.\n")
+				} else {
+					panic(r)
+				}
 			}
 		}
 	}()
@@ -164,14 +169,14 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 				return fmt.Errorf(permissionsDeniedMessage, newFile)
 			}
 
-			defer dst.Close()
 			src, err := zf.Open()
 			if err != nil {
-				log.Errorf("%v", err)
+				log.Errorf("Failed to extract file: %v", err)
 			}
-			defer src.Close()
 
 			io.Copy(dst, src)
+			dst.Close()
+			src.Close()
 		}
 	}
 

+ 3 - 8
pkg/cmd/grafana-cli/commands/remove_command.go

@@ -3,7 +3,7 @@ package commands
 import (
 	"errors"
 
-	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
+	"fmt"
 	m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
 	services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
 )
@@ -15,22 +15,17 @@ func removeCommand(c CommandLine) error {
 	pluginPath := c.GlobalString("pluginsDir")
 	localPlugins := getPluginss(pluginPath)
 
-	log.Info("remove!\n")
-
 	plugin := c.Args().First()
-	log.Info("plugin: " + plugin + "\n")
 	if plugin == "" {
 		return errors.New("Missing plugin parameter")
 	}
 
-	log.Infof("plugins : \n%v\n", localPlugins)
-
 	for _, p := range localPlugins {
 		if p.Id == c.Args().First() {
-			log.Infof("removing plugin %s", p.Id)
 			removePlugin(pluginPath, p.Id)
+			return nil
 		}
 	}
 
-	return nil
+	return fmt.Errorf("Could not find plugin named %s", c.Args().First())
 }

+ 5 - 5
pkg/cmd/grafana-cli/main.go

@@ -8,7 +8,6 @@ import (
 	"github.com/codegangsta/cli"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
-	"strings"
 )
 
 var version = "master"
@@ -18,7 +17,7 @@ func getGrafanaPluginDir() string {
 	defaultNix := "/var/lib/grafana/plugins"
 
 	if currentOS == "windows" {
-		return "C:\\opt\\grafana\\plugins"
+		return "../data/plugins"
 	}
 
 	pwd, err := os.Getwd()
@@ -29,16 +28,17 @@ func getGrafanaPluginDir() string {
 	}
 
 	if isDevenvironment(pwd) {
-		return "../../../data/plugins"
+		return "../data/plugins"
 	}
 
 	return defaultNix
 }
 
 func isDevenvironment(pwd string) bool {
-	// if grafana-cli is executed from the cmd folder we can assume
+	// if ../conf/defaults.ini exists, grafana is not installed as package
 	// that its in development environment.
-	return strings.HasSuffix(pwd, "/pkg/cmd/grafana-cli")
+	_, err := os.Stat("../conf/defaults.ini")
+	return err == nil
 }
 
 func main() {

+ 1 - 1
pkg/models/dashboard_snapshot.go

@@ -45,7 +45,7 @@ type DashboardSnapshotDTO struct {
 
 type CreateDashboardSnapshotCommand struct {
 	Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
-	Name      string           `json:"name" binding:"Required"`
+	Name      string           `json:"name"`
 	Expires   int64            `json:"expires"`
 
 	// these are passed when storing an external snapshot ref

+ 22 - 0
pkg/models/org_user.go

@@ -1,7 +1,9 @@
 package models
 
 import (
+	"encoding/json"
 	"errors"
+	"fmt"
 	"time"
 )
 
@@ -37,6 +39,26 @@ func (r RoleType) Includes(other RoleType) bool {
 	return r == other
 }
 
+func (r *RoleType) UnmarshalJSON(data []byte) error {
+	var str string
+	err := json.Unmarshal(data, &str)
+	if err != nil {
+		return err
+	}
+
+	*r = RoleType(str)
+
+	if (*r).IsValid() == false {
+		if (*r) != "" {
+			return errors.New(fmt.Sprintf("JSON validation error: invalid role value: %s", *r))
+		}
+
+		*r = ROLE_VIEWER
+	}
+
+	return nil
+}
+
 type OrgUser struct {
 	Id      int64
 	OrgId   int64

+ 7 - 5
pkg/plugins/dashboard_importer.go

@@ -11,12 +11,13 @@ import (
 )
 
 type ImportDashboardCommand struct {
-	Path   string                 `json:"string"`
-	Inputs []ImportDashboardInput `json:"inputs"`
+	Path      string
+	Inputs    []ImportDashboardInput
+	Overwrite bool
 
-	OrgId    int64  `json:"-"`
-	UserId   int64  `json:"-"`
-	PluginId string `json:"-"`
+	OrgId    int64
+	UserId   int64
+	PluginId string
 	Result   *PluginDashboardInfoDTO
 }
 
@@ -67,6 +68,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
 		Dashboard: generatedDash,
 		OrgId:     cmd.OrgId,
 		UserId:    cmd.UserId,
+		Overwrite: cmd.Overwrite,
 	}
 
 	if err := bus.Dispatch(&saveCmd); err != nil {

+ 15 - 9
pkg/plugins/models.go

@@ -7,7 +7,7 @@ import (
 	"strings"
 
 	"github.com/grafana/grafana/pkg/log"
-	"github.com/grafana/grafana/pkg/models"
+	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -69,6 +69,12 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error {
 		pb.Dependencies.GrafanaVersion = "*"
 	}
 
+	for _, include := range pb.Includes {
+		if include.Role == "" {
+			include.Role = m.RoleType(m.ROLE_VIEWER)
+		}
+	}
+
 	pb.PluginDir = pluginDir
 	Plugins[pb.Id] = pb
 	return nil
@@ -80,14 +86,14 @@ type PluginDependencies struct {
 }
 
 type PluginInclude struct {
-	Name       string          `json:"name"`
-	Path       string          `json:"path"`
-	Type       string          `json:"type"`
-	Component  string          `json:"component"`
-	Role       models.RoleType `json:"role"`
-	AddToNav   bool            `json:"addToNav"`
-	DefaultNav bool            `json:"defaultNav"`
-	Slug       string          `json:"slug"`
+	Name       string     `json:"name"`
+	Path       string     `json:"path"`
+	Type       string     `json:"type"`
+	Component  string     `json:"component"`
+	Role       m.RoleType `json:"role"`
+	AddToNav   bool       `json:"addToNav"`
+	DefaultNav bool       `json:"defaultNav"`
+	Slug       string     `json:"slug"`
 
 	Id string `json:"-"`
 }

+ 1 - 1
pkg/services/sqlstore/dashboard.go

@@ -216,7 +216,7 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
 func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 	return inTransaction2(func(sess *session) error {
 		dashboard := m.Dashboard{Slug: cmd.Slug, OrgId: cmd.OrgId}
-		has, err := x.Get(&dashboard)
+		has, err := sess.Get(&dashboard)
 		if err != nil {
 			return err
 		} else if has == false {

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

@@ -61,7 +61,6 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
 			for key, data := range cmd.SecureJsonData {
 				pluginSetting.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
 			}
-			pluginSetting.SecureJsonData = cmd.GetEncryptedJsonData()
 			pluginSetting.Updated = time.Now()
 			pluginSetting.Enabled = cmd.Enabled
 			pluginSetting.JsonData = cmd.JsonData

+ 3 - 0
public/app/core/components/dashboard_selector.ts

@@ -7,6 +7,9 @@ import coreModule from 'app/core/core_module';
 
 var template = `
 <select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select>
+<info-popover mode="right-absolute">
+  Not finding dashboard you want? Star it first, then it should appear in this select box.
+</info-popover>
 `;
 
 export class DashboardSelectorCtrl {

+ 12 - 8
public/app/core/components/info_popover.ts

@@ -8,21 +8,24 @@ import Drop from 'tether-drop';
 export function infoPopover() {
   return {
     restrict: 'E',
+    template: '<i class="fa fa-info-circle"></i>',
     transclude: true,
     link: function(scope, elem, attrs, ctrl, transclude) {
-      var inputElem = elem.prev();
-      if (inputElem.length === 0) {
-        console.log('Failed to find input element for popover');
-        return;
-      }
-
       var offset = attrs.offset || '0 -10px';
       var position = attrs.position || 'right middle';
       var classes = 'drop-help drop-hide-out-of-bounds';
+      var openOn = 'hover';
+
+      elem.addClass('gf-form-help-icon');
+
       if (attrs.wide) {
         classes += ' drop-wide';
       }
 
+      if (attrs.mode) {
+        elem.addClass('gf-form-help-icon--' + attrs.mode);
+      }
+
       transclude(function(clone, newScope) {
         var content = document.createElement("div");
         _.each(clone, (node) => {
@@ -30,11 +33,12 @@ export function infoPopover() {
         });
 
         var drop = new Drop({
-          target: inputElem[0],
+          target: elem[0],
           content: content,
           position: position,
           classes: classes,
-          openOn: 'click',
+          openOn: openOn,
+          hoverOpenDelay: 400,
           tetherOptions: {
             offset: offset
           }

+ 6 - 17
public/app/core/components/switch.ts

@@ -7,7 +7,12 @@ import coreModule from 'app/core/core_module';
 import Drop from 'tether-drop';
 
 var template = `
-<label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer">{{ctrl.label}}</label>
+<label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer">
+  {{ctrl.label}}
+  <info-popover mode="right-normal" ng-if="ctrl.tooltip">
+    {{ctrl.tooltip}}
+  </info-popover>
+</label>
 <div class="gf-form-switch {{ctrl.switchClass}}" ng-if="ctrl.show">
   <input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()">
   <label for="check-{{ctrl.id}}" data-on="Yes" data-off="No"></label>
@@ -49,22 +54,6 @@ export function switchDirective() {
       onChange: "&",
     },
     template: template,
-    link: (scope, elem) => {
-      if (scope.ctrl.tooltip) {
-        var drop = new Drop({
-          target: elem[0],
-          content: scope.ctrl.tooltip,
-          position: "right middle",
-          classes: 'drop-help',
-          openOn: 'hover',
-          hoverOpenDelay: 400,
-        });
-
-        scope.$on('$destroy', function() {
-          drop.destroy();
-        });
-      }
-    }
   };
 }
 

+ 2 - 2
public/app/core/directives/dropdown_typeahead.js

@@ -9,10 +9,10 @@ function (_, $, coreModule) {
   coreModule.default.directive('dropdownTypeahead', function($compile) {
 
     var inputTemplate = '<input type="text"'+
-      ' class="tight-form-input input-medium tight-form-input"' +
+      ' class="gf-form-input input-medium tight-form-input"' +
       ' spellcheck="false" style="display:none"></input>';
 
-    var buttonTemplate = '<a  class="tight-form-item tight-form-func dropdown-toggle"' +
+    var buttonTemplate = '<a  class="gf-form-label tight-form-func dropdown-toggle"' +
       ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
       ' data-placement="top"><i class="fa fa-plus"></i></a>';
 

+ 7 - 4
public/app/core/directives/metric_segment.js

@@ -8,10 +8,13 @@ function (_, $, coreModule) {
 
   coreModule.default.directive('metricSegment', function($compile, $sce) {
     var inputTemplate = '<input type="text" data-provide="typeahead" ' +
-      ' class="tight-form-clear-input input-medium"' +
+      ' class="gf-form-input input-medium"' +
       ' spellcheck="false" style="display:none"></input>';
 
-    var buttonTemplate = '<a class="tight-form-item" ng-class="segment.cssClass" ' +
+    var linkTemplate = '<a class="gf-form-label" ng-class="segment.cssClass" ' +
+      'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
+
+    var selectTemplate = '<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
       'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
 
     return {
@@ -20,9 +23,9 @@ function (_, $, coreModule) {
         getOptions: "&",
         onChange: "&",
       },
-      link: function($scope, elem) {
+      link: function($scope, elem, attrs) {
         var $input = $(inputTemplate);
-        var $button = $(buttonTemplate);
+        var $button = $(attrs.styleMode === 'select' ? selectTemplate : linkTemplate);
         var segment = $scope.segment;
         var options = null;
         var cancelBlur = null;

+ 8 - 2
public/app/core/directives/plugin_component.ts

@@ -206,9 +206,15 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
     });
 
     $compile(child)(scope);
-
     elem.empty();
-    elem.append(child);
+
+    // let a binding digest cycle complete before adding to dom
+    setTimeout(function() {
+      elem.append(child);
+      scope.$apply(function() {
+        scope.$broadcast('refresh');
+      });
+    });
   }
 
   function registerPluginComponent(scope, elem, attrs, componentInfo) {

+ 0 - 2
public/app/core/services/context_srv.ts

@@ -25,7 +25,6 @@ export class ContextSrv {
   isGrafanaAdmin: any;
   isEditor: any;
   sidemenu: any;
-  lightTheme: any;
 
   constructor() {
     this.pinned = store.getBool('grafana.sidemenu.pinned', false);
@@ -41,7 +40,6 @@ export class ContextSrv {
     }
 
     this.version = config.buildInfo.version;
-    this.lightTheme = false;
     this.user = new User();
     this.isSignedIn = this.user.isSignedIn;
     this.isGrafanaAdmin = this.user.isGrafanaAdmin;

+ 56 - 29
public/app/core/services/datasource_srv.js

@@ -7,36 +7,11 @@ define([
 function (angular, _, coreModule, config) {
   'use strict';
 
-  coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope) {
+  coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope, templateSrv) {
     var self = this;
 
     this.init = function() {
       this.datasources = {};
-      this.metricSources = [];
-      this.annotationSources = [];
-
-      _.each(config.datasources, function(value, key) {
-        if (value.meta && value.meta.metrics) {
-          self.metricSources.push({
-            value: key === config.defaultDatasource ? null : key,
-            name: key,
-            meta: value.meta,
-          });
-        }
-        if (value.meta && value.meta.annotations) {
-          self.annotationSources.push(value);
-        }
-      });
-
-      this.metricSources.sort(function(a, b) {
-        if (a.meta.builtIn || a.name > b.name) {
-          return 1;
-        }
-        if (a.name < b.name) {
-          return -1;
-        }
-        return 0;
-      });
     };
 
     this.get = function(name) {
@@ -44,6 +19,8 @@ function (angular, _, coreModule, config) {
         return this.get(config.defaultDatasource);
       }
 
+      name = templateSrv.replace(name);
+
       if (this.datasources[name]) {
         return $q.when(this.datasources[name]);
       }
@@ -89,11 +66,61 @@ function (angular, _, coreModule, config) {
     };
 
     this.getAnnotationSources = function() {
-      return this.annotationSources;
+      return _.reduce(config.datasources, function(memo, key, value) {
+
+        if (value.meta && value.meta.annotations) {
+          memo.push(value);
+        }
+
+        return memo;
+      }, []);
     };
 
-    this.getMetricSources = function() {
-      return this.metricSources;
+    this.getMetricSources = function(options) {
+      var metricSources = [];
+
+      _.each(config.datasources, function(value, key) {
+        if (value.meta && value.meta.metrics) {
+          metricSources.push({
+            value: key === config.defaultDatasource ? null : key,
+            name: key,
+            meta: value.meta,
+          });
+        }
+      });
+
+      if (!options || !options.skipVariables) {
+        // look for data source variables
+        for (var i = 0; i < templateSrv.variables.length; i++) {
+          var variable = templateSrv.variables[i];
+          if (variable.type !== 'datasource') {
+            continue;
+          }
+
+          var first = variable.current.value;
+          var ds = config.datasources[first];
+
+          if (ds) {
+            metricSources.push({
+              name: '$' + variable.name,
+              value: '$' + variable.name,
+              meta: ds.meta,
+            });
+          }
+        }
+      }
+
+      metricSources.sort(function(a, b) {
+        if (a.meta.builtIn || a.name > b.name) {
+          return 1;
+        }
+        if (a.name < b.name) {
+          return -1;
+        }
+        return 0;
+      });
+
+      return metricSources;
     };
 
     this.init();

+ 1 - 1
public/app/core/services/segment_srv.js

@@ -19,7 +19,7 @@ function (angular, _, coreModule) {
 
       if (_.isString(options)) {
         this.value = options;
-        this.html = $sce.trustAsHtml(this.value);
+        this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
         return;
       }
 

+ 2 - 0
public/app/core/table_model.ts

@@ -32,6 +32,8 @@ export default class TableModel {
     if (options.desc) {
       this.rows.reverse();
       this.columns[options.col].desc = true;
+    } else {
+      this.columns[options.col].desc = false;
     }
   }
 }

+ 1 - 1
public/app/core/utils/emitter.ts

@@ -23,7 +23,7 @@ export class Emitter {
     this.emitter.on(name, handler);
 
     if (scope) {
-      scope.$on('$destroy', function() {
+      scope.$on('$destroy', () => {
         this.emitter.off(name, handler);
       });
     }

+ 7 - 4
public/app/features/dashboard/partials/settings.html

@@ -26,7 +26,10 @@
 				<input type="text" class="gf-form-input width-25" ng-model='dashboard.title'></input>
 			</div>
 			<div class="gf-form">
-				<label class="gf-form-label width-7">Tags<tip>Press enter to a add tag</tip></label>
+				<label class="gf-form-label width-7">
+          Tags
+          <info-popover mode="right-normal">Press enter to a add tag</info-popover>
+        </label>
 				<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
 				</bootstrap-tagsinput>
 			</div>
@@ -46,19 +49,19 @@
                         label="Editable"
                         tooltip="Uncheck, then save and reload to disable all dashboard editing"
                         checked="dashboard.editable"
-                        label-class="width-10">
+                        label-class="width-11">
         </gf-form-switch>
         <gf-form-switch class="gf-form"
                         label="Build Mode"
                         tooltip="Enable build mode. Shortcut: CTRL+B"
                         checked="dashboard.editMode"
-                        label-class="width-10">
+                        label-class="width-11">
         </gf-form-switch>
         <gf-form-switch class="gf-form"
                         label="Shared Crosshair"
                         tooltip="Shared Crosshair line on all graphs. Shortcut: CTRL+O"
                         checked="dashboard.sharedCrosshair"
-                        label-class="width-10">
+                        label-class="width-11">
         </gf-form-switch>
       </div>
     </div>

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

@@ -3,7 +3,7 @@
 	<li class="dashnav-move-timeframe gf-timepicker-time-control" bs-tooltip="'Shift time backward <br> (left arrow key)'" data-placement="bottom">
 		<a ng-click='ctrl.move(-1)'><i class="fa fa-chevron-left"></i></a>
 	</li>
-	<li class="dashnav-zoom-out gf-timepicker-time-control" bs-tooltip="'Time range zoom in <br> CTRL+Z'" data-placement="bottom">
+	<li class="dashnav-zoom-out gf-timepicker-time-control" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom">
 		<a ng-click='ctrl.zoom(2)'>Zoom Out</a></li>
 	</li>
 	<li class="dashnav-move-timeframe gf-timepicker-time-control" bs-tooltip="'Shift time forward <br> (right arrow key)'" data-placement="bottom">

+ 1 - 1
public/app/features/dashboard/timepicker/timepicker.ts

@@ -92,7 +92,7 @@ export class TimePickerCtrl {
   move(direction) {
     var range = this.timeSrv.timeRange();
 
-    var timespan = (range.to.valueOf() - range.from.valueOf());
+    var timespan = (range.to.valueOf() - range.from.valueOf()) / 2;
     var to, from;
     if (direction === -1) {
       to = range.to.valueOf() - timespan;

+ 1 - 1
public/app/features/dashboard/viewStateSrv.js

@@ -36,7 +36,7 @@ function (angular, _, $) {
         self.update(payload);
       });
 
-      $scope.onAppEvent('panel-instantiated', function(evt, payload) {
+      $scope.onAppEvent('panel-initialized', function(evt, payload) {
         self.registerPanel(payload.scope);
       });
 

+ 3 - 4
public/app/features/org/prefs_control.ts

@@ -49,7 +49,7 @@ export class PrefsControlCtrl {
 }
 
 var template = `
-<form name="ctrl.prefsForm" class="gf-form-group">
+<form name="ctrl.prefsForm" class="section gf-form-group">
   <h3 class="page-heading">Preferences</h3>
 
   <div class="gf-form">
@@ -61,9 +61,8 @@ var template = `
 
   <div class="gf-form">
     <span class="gf-form-label width-9">Home Dashboard</span>
-    <dashboard-selector
-        class="gf-form-select-wrapper max-width-20"
-        model="ctrl.prefs.homeDashboardId">
+    <dashboard-selector class="gf-form-select-wrapper max-width-20 gf-form-select-wrapper--has-help-icon"
+                        model="ctrl.prefs.homeDashboardId">
     </dashboard-selector>
   </div>
 

+ 1 - 0
public/app/features/panel/all.js

@@ -5,4 +5,5 @@ define([
   './query_ctrl',
   './panel_editor_tab',
   './query_editor_row',
+  './metrics_ds_selector',
 ], function () {});

+ 110 - 0
public/app/features/panel/metrics_ds_selector.ts

@@ -0,0 +1,110 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+var module = angular.module('grafana.directives');
+
+var template = `
+<div class="gf-form-group">
+  <div class="gf-form-inline">
+    <div class="gf-form">
+      <label class="gf-form-label">
+        <i class="icon-gf icon-gf-datasource"></i>
+      </label>
+      <label class="gf-form-label">
+        Panel data source
+      </label>
+
+      <metric-segment segment="ctrl.dsSegment" style-mode="select"
+                      get-options="ctrl.getOptions()"
+                      on-change="ctrl.datasourceChanged()"></metric-segment>
+    </div>
+
+    <div class="gf-form gf-form--offset-1">
+      <button class="btn btn-inverse gf-form-btn" ng-click="ctrl.addDataQuery()" ng-hide="ctrl.current.meta.mixed">
+        <i class="fa fa-plus"></i>&nbsp;
+        Add query
+      </button>
+
+      <div class="dropdown" ng-if="ctrl.current.meta.mixed">
+        <button class="btn btn-inverse dropdown-toggle gf-form-btn" data-toggle="dropdown">
+          Add Query&nbsp;<span class="fa fa-caret-down"></span>
+        </button>
+
+        <ul class="dropdown-menu" role="menu">
+          <li ng-repeat="datasource in ctrl.datasources" role="menuitem" ng-hide="datasource.meta.builtIn">
+            <a ng-click="ctrl.addDataQuery(datasource);">{{datasource.name}}</a>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+
+export class MetricsDsSelectorCtrl {
+  dsSegment: any;
+  dsName: string;
+  panelCtrl: any;
+  datasources: any[];
+  current: any;
+
+  /** @ngInject */
+  constructor(private uiSegmentSrv, datasourceSrv) {
+    this.datasources = datasourceSrv.getMetricSources();
+
+    var dsValue = this.panelCtrl.panel.datasource || null;
+
+    for (let ds of this.datasources) {
+      if (ds.value === dsValue) {
+        this.current = ds;
+      }
+    }
+
+    if (!this.current) {
+      this.current = {name: dsValue + ' not found', value: null};
+    }
+
+    this.dsSegment = uiSegmentSrv.newSegment(this.current.name);
+  }
+
+  getOptions() {
+    return Promise.resolve(this.datasources.map(value => {
+      return this.uiSegmentSrv.newSegment(value.name);
+    }));
+  }
+
+  datasourceChanged() {
+    var ds = _.findWhere(this.datasources, {name: this.dsSegment.value});
+    if (ds) {
+      this.current = ds;
+      this.panelCtrl.setDatasource(ds);
+    }
+  }
+
+  addDataQuery(datasource) {
+    var target: any = {isNew: true};
+
+    if (datasource) {
+      target.datasource = datasource.name;
+    }
+
+    this.panelCtrl.panel.targets.push(target);
+  }
+}
+
+module.directive('metricsDsSelector', function() {
+  return {
+    restrict: 'E',
+    template: template,
+    controller: MetricsDsSelectorCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    transclude: true,
+    scope: {
+      panelCtrl: "="
+    }
+  };
+});

+ 2 - 12
public/app/features/panel/metrics_panel_ctrl.ts

@@ -15,6 +15,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   error: boolean;
   loading: boolean;
   datasource: any;
+  datasourceName: any;
   $q: any;
   $timeout: any;
   datasourceSrv: any;
@@ -27,7 +28,6 @@ class MetricsPanelCtrl extends PanelCtrl {
   resolution: any;
   timeInfo: any;
   skipDataOnInit: boolean;
-  datasources: any[];
   dataStream: any;
   dataSubscription: any;
 
@@ -52,7 +52,6 @@ class MetricsPanelCtrl extends PanelCtrl {
   private onInitMetricsPanelEditMode() {
     this.addEditorTab('Metrics', 'public/app/partials/metrics.html');
     this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
-    this.datasources = this.datasourceSrv.getMetricSources();
   }
 
   private onMetricsPanelRefresh() {
@@ -246,19 +245,10 @@ class MetricsPanelCtrl extends PanelCtrl {
     }
 
     this.panel.datasource = datasource.value;
+    this.datasourceName = datasource.name;
     this.datasource = null;
     this.refresh();
   }
-
-  addDataQuery(datasource) {
-    var target: any = {};
-
-    if (datasource) {
-      target.datasource = datasource.name;
-    }
-
-    this.panel.targets.push(target);
-  }
 }
 
 export {MetricsPanelCtrl};

+ 2 - 2
public/app/features/panel/panel_ctrl.ts

@@ -50,9 +50,9 @@ export class PanelCtrl {
   }
 
   init() {
-    this.publishAppEvent('panel-instantiated', {scope: this.$scope});
     this.calculatePanelHeight();
-    this.refresh();
+    this.publishAppEvent('panel-initialized', {scope: this.$scope});
+    this.events.emit('panel-initialized');
   }
 
   renderingCompleted() {

+ 60 - 1
public/app/features/panel/partials/query_editor_row.html

@@ -1,4 +1,63 @@
-<div class="tight-form">
+
+<div class="gf-form-query">
+	<div class="gf-form">
+    <label class="gf-form-label gf-form-query-letter-cell">
+      <a class="pointer" tabindex="1" ng-click="ctrl.toggleCollapse()">
+        <span  ng-class="{muted: !ctrl.canCollapse}" class="gf-form-query-letter-cell-carret">
+          <i class="fa fa-caret-down" ng-hide="ctrl.collapsed"></i>
+          <i class="fa fa-caret-right" ng-show="ctrl.collapsed"></i>
+        </span>
+        <span class="gf-form-query-letter-cell-letter">{{ctrl.target.refId}}</span>
+        <em class="gf-form-query-letter-cell-ds" ng-show="ctrl.target.datasource">({{ctrl.target.datasource}})</em>
+      </a>
+		</label>
+  </div>
+
+	<div class="gf-form-query-content gf-form-query-content--collapsed" ng-if="ctrl.collapsed">
+		<div class="gf-form">
+			<label class="gf-form-label pointer gf-form-label--grow" ng-click="ctrl.toggleCollapse()">
+				{{ctrl.collapsedText}}
+			</label>
+		</div>
+	</div>
+
+	<div ng-transclude class="gf-form-query-content" ng-if="!ctrl.collapsed">
+	</div>
+
+	<div class="gf-form">
+		<label class="gf-form-label dropdown">
+			<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
+				<i class="fa fa-bars"></i>
+			</a>
+			<ul class="dropdown-menu pull-right" role="menu">
+				<li role="menuitem" ng-if="ctrl.hasTextEditMode">
+					<a tabindex="1" ng-click="ctrl.toggleEditorMode()">Toggle Edit Mode</a>
+				</li>
+				<li role="menuitem">
+					<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
+				</li>
+				<li role="menuitem">
+					<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
+				</li>
+				<li role="menuitem">
+					<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
+				</li>
+			</ul>
+		</label>
+    <label class="gf-form-label">
+			<a ng-click="ctrl.toggleHideQuery()" role="menuitem">
+				<i class="fa fa-eye"></i>
+			</a>
+		</label>
+		<label class="gf-form-label">
+			<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(ctrl.target)">
+				<i class="fa fa-trash"></i>
+			</a>
+		</label>
+	</div>
+</div>
+
+<div class="tight-form" ng-if="false">
 	<ul class="tight-form-list pull-right">
 		<li ng-show="ctrl.error" class="tight-form-item">
 			<a bs-tooltip="ctrl.error" style="color: rgb(229, 189, 28)" role="menuitem">

+ 0 - 34
public/app/features/panel/query_ctrl.ts

@@ -13,45 +13,11 @@ export class QueryCtrl {
 
   constructor(public $scope, private $injector) {
     this.panel = this.panelCtrl.panel;
-
-    if (!this.target.refId) {
-      this.target.refId = this.getNextQueryLetter();
-    }
-  }
-
-  getNextQueryLetter() {
-    var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-    return _.find(letters, refId => {
-      return _.every(this.panel.targets, function(other) {
-        return other.refId !== refId;
-      });
-    });
-  }
-
-  removeQuery() {
-    this.panel.targets = _.without(this.panel.targets, this.target);
-    this.panelCtrl.refresh();
-  };
-
-  duplicateQuery() {
-    var clone = angular.copy(this.target);
-    clone.refId = this.getNextQueryLetter();
-    this.panel.targets.push(clone);
-  }
-
-  moveQuery(direction) {
-    var index = _.indexOf(this.panel.targets, this.target);
-    _.move(this.panel.targets, index, index + direction);
   }
 
   refresh() {
     this.panelCtrl.refresh();
   }
 
-  toggleHideQuery() {
-    this.target.hide = !this.target.hide;
-    this.panelCtrl.refresh();
-  }
 }
 

+ 100 - 2
public/app/features/panel/query_editor_row.ts

@@ -1,17 +1,115 @@
 ///<reference path="../../headers/common.d.ts" />
 
 import angular from 'angular';
-import $ from 'jquery';
+import _ from 'lodash';
 
 var module = angular.module('grafana.directives');
 
+export class QueryRowCtrl {
+  collapsedText: string;
+  canCollapse: boolean;
+  getCollapsedText: any;
+  target: any;
+  queryCtrl: any;
+  panelCtrl: any;
+  panel: any;
+  collapsed: any;
+
+  constructor() {
+    this.panelCtrl = this.queryCtrl.panelCtrl;
+    this.target = this.queryCtrl.target;
+    this.panel = this.panelCtrl.panel;
+
+    if (!this.target.refId) {
+      this.target.refId = this.getNextQueryLetter();
+    }
+
+    this.toggleCollapse(true);
+    if (this.target.isNew) {
+      delete this.target.isNew;
+      this.toggleCollapse(false);
+    }
+  }
+
+  toggleHideQuery() {
+    this.target.hide = !this.target.hide;
+    this.panelCtrl.refresh();
+  }
+
+  getNextQueryLetter() {
+    var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+    return _.find(letters, refId => {
+      return _.every(this.panel.targets, function(other) {
+        return other.refId !== refId;
+      });
+    });
+  }
+
+  toggleCollapse(init) {
+    if (!this.canCollapse) {
+      return;
+    }
+
+    if (!this.panelCtrl.__collapsedQueryCache) {
+      this.panelCtrl.__collapsedQueryCache = {};
+    }
+
+    if (init) {
+      this.collapsed = this.panelCtrl.__collapsedQueryCache[this.target.refId] !== false;
+    } else {
+      this.collapsed = !this.collapsed;
+      this.panelCtrl.__collapsedQueryCache[this.target.refId] = this.collapsed;
+    }
+
+    try {
+      this.collapsedText = this.queryCtrl.getCollapsedText();
+    } catch (e) {
+      var err = e.message || e.toString();
+      this.collapsedText = 'Error: ' + err;
+    }
+  }
+
+  toggleEditorMode() {
+    if (this.canCollapse && this.collapsed) {
+      this.collapsed = false;
+    }
+
+    this.queryCtrl.toggleEditorMode();
+  }
+
+  removeQuery() {
+    delete this.panelCtrl.__collapsedQueryCache[this.target.refId];
+    this.panel.targets = _.without(this.panel.targets, this.target);
+    this.panelCtrl.refresh();
+  }
+
+  duplicateQuery() {
+    var clone = angular.copy(this.target);
+    clone.refId = this.getNextQueryLetter();
+    this.panel.targets.push(clone);
+  }
+
+  moveQuery(direction) {
+    var index = _.indexOf(this.panel.targets, this.target);
+    _.move(this.panel.targets, index, index + direction);
+  }
+}
+
 /** @ngInject **/
 function queryEditorRowDirective() {
   return {
     restrict: 'E',
+    controller: QueryRowCtrl,
+    bindToController: true,
+    controllerAs: "ctrl",
     templateUrl: 'public/app/features/panel/partials/query_editor_row.html',
     transclude: true,
-    scope: {ctrl: "="},
+    scope: {
+      queryCtrl: "=",
+      canCollapse: "=",
+      hasTextEditMode: "=",
+    },
   };
 }
 

+ 4 - 4
public/app/features/plugins/ds_edit_ctrl.ts

@@ -98,9 +98,7 @@ export class DataSourceEditCtrl {
 
       this.datasourceSrv.get(this.current.name).then(datasource => {
         if (!datasource.testDatasource) {
-          this.testing.message = 'Data source does not support test connection feature.';
-          this.testing.status = 'warning';
-          this.testing.title = 'Unknown';
+          delete this.testing;
           return;
         }
 
@@ -118,7 +116,9 @@ export class DataSourceEditCtrl {
           }
         });
       }).finally(() => {
-        this.testing.done = true;
+        if (this.testing) {
+          this.testing.done = true;
+        }
       });
     }
 

+ 4 - 4
public/app/features/plugins/import_list/import_list.html

@@ -15,16 +15,16 @@
 				</td>
 				<td>
 					v{{dash.revision}}
-				</td>
-				<td ng-if="dash.installed">
-					Imported v{{dash.installedRevision}}
+					<span ng-if="dash.installed">
+						&nbsp;(Imported v{{dash.installedRevision}})
+					<span>
 				</td>
 				<td style="text-align: right">
 					<button class="btn btn-secondary" ng-click="ctrl.import(dash, false)" ng-show="!dash.installed">
 						Import
 					</button>
 					<button class="btn btn-secondary" ng-click="ctrl.import(dash, true)" ng-show="dash.installed">
-						Re-Import
+						Update
 					</button>
 					<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.installed">
 						Delete

+ 2 - 2
public/app/features/plugins/import_list/import_list.ts

@@ -43,11 +43,11 @@ export class DashImportListCtrl {
     });
   }
 
-  import(dash, reinstall) {
+  import(dash, overwrite) {
     var installCmd = {
       pluginId: this.plugin.id,
       path: dash.path,
-      reinstall: reinstall,
+      overwrite: overwrite,
       inputs: []
     };
 

+ 7 - 7
public/app/features/plugins/partials/ds_edit.html

@@ -31,18 +31,18 @@
 					<div class="gf-form">
 						<span class="gf-form-label width-7">Name</span>
 						<input class="gf-form-input max-width-21" type="text" ng-model="ctrl.current.name" placeholder="My data source name" required>
+						<info-popover offset="0px -135px" mode="right-absolute">
+							The name is used when you select the data source in panels.
+							The <em>Default</em> data source is preselected in new
+							panels.
+						</info-popover>
 					</div>
-					<info-popover offset="0px -130px">
-						The name is used when you select the data source in panels.
-						The <em>Default</em> data source is preselected in new
-						panels.
-					</info-popover>
 					<gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
 				</div>
 
 				<div class="gf-form">
 					<span class="gf-form-label width-7">Type</span>
-					<div class="gf-form-select-wrapper max-width-21">
+					<div class="gf-form-select-wrapper max-width-23">
 						<select class="gf-form-input" ng-model="ctrl.current.type" ng-options="v.id as v.name for v in ctrl.types" ng-change="ctrl.typeChanged()"></select>
 					</div>
 				</div>
@@ -53,7 +53,7 @@
 			</plugin-component>
 			</rebuild-on-change>
 
-			<div ng-if="ctrl.testing" style="margin-top: 25px">
+			<div ng-if="ctrl.testing" class="gf-form-group">
 				<h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
 				<div class="alert-{{ctrl.testing.status}} alert">
 					<div class="alert-title">{{ctrl.testing.title}}</div>

+ 30 - 25
public/app/features/plugins/partials/ds_http_settings.html

@@ -3,29 +3,34 @@
 <div class="gf-form-group">
 	<h3 class="page-heading">Http settings</h3>
 
-	<div class="gf-form">
-		<span class="gf-form-label width-7">Url</span>
-		<input class="gf-form-input max-width-21" type="text" ng-model='current.url' placeholder="for example: http://localhost:8081" ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
-
-		<info-popover>
-			<p>Specify a complete HTTP url (for example http://your_server:8080)</p>
-			<span ng-show="current.access === 'direct'">
-				Your access method is <em>Direct</em>, this means the url
-				needs to be accessable from the browser.
-			</span>
-			<span ng-show="current.access === 'proxy'">
-				Your access method is currently <em>Proxy</em>, this means the url
-				needs to be accessable from the grafana backend.
-			</span>
-		</info-popover>
+	<div class="gf-form-inline">
+		<div class="gf-form max-width-30">
+			<span class="gf-form-label width-7">Url</span>
+			<input class="gf-form-input" type="text" ng-model='current.url' placeholder="for example: http://localhost:8081" ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
+			<info-popover mode="right-absolute">
+				<p>Specify a complete HTTP url (for example http://your_server:8080)</p>
+				<span ng-show="current.access === 'direct'">
+					Your access method is <em>Direct</em>, this means the url
+					needs to be accessable from the browser.
+				</span>
+				<span ng-show="current.access === 'proxy'">
+					Your access method is currently <em>Proxy</em>, this means the url
+					needs to be accessable from the grafana backend.
+				</span>
+			</info-popover>
+		</div>
 	</div>
 
-	<div class="gf-form">
-		<span class="gf-form-label width-7">
-			Access <tip>Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request</tip>
-		</span>
-		<div class="gf-form-select-wrapper">
-			<select class="gf-form-input gf-size-auto" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
+	<div class="gf-form-inline">
+		<div class="gf-form max-width-30">
+			<span class="gf-form-label width-7">Access</span>
+			<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
+				<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
+				<info-popover mode="right-absolute">
+					Direct = url is used directly from browser<br>
+					Proxy = Grafana backend will proxy the request
+				</info-popover>
+			</div>
 		</div>
 	</div>
 
@@ -34,12 +39,12 @@
 			<label class="gf-form-label width-7">Http Auth</label>
 		</div>
 		<gf-form-switch class="gf-form"
-			label="Basic Auth"
-			checked="current.basicAuth" switch-class="max-width-6">
+									label="Basic Auth"
+				 checked="current.basicAuth" switch-class="max-width-6">
 		</gf-form-switch>
 		<gf-form-switch class="gf-form"
-			label="With Credentials"
-			checked="current.withCredentials" switch-class="max-width-6">
+									label="With Credentials"
+				 checked="current.withCredentials" switch-class="max-width-6">
 		</gf-form-switch>
 	</div>
 

+ 2 - 2
public/app/features/plugins/partials/update_instructions.html

@@ -14,8 +14,8 @@
 		<div class="gf-form-group">
 			<p>Type the following on the command line to update {{plugin.name}}.</p>
 			<pre><code>grafana-cli plugins update {{plugin.id}}</code></pre>
-			<span class="small">Check out {{plugin.name}} on <a href="http://grafana/net/plugins/{{plugin.id}}">Grafana.net</a> for README and changelog. If you do not have access to the command line, ask your Grafana administator.</span>
+			<span class="small">Check out {{plugin.name}} on <a href="https://grafana.net/plugins/{{plugin.id}}">Grafana.net</a> for README and changelog. If you do not have access to the command line, ask your Grafana administator.</span>
 		</div>
-		<p class="pluginlist-none-installed code--line"><img class="pluginlist-inline-logo" src="public/img/grafana_icon.svg"><strong>Pro tip</strong>: To update all plugins at once, type <code class="code--small">grafana-cli plugins update-all</code> on the command line.</div>
+		<p class="pluginlist-none-installed"><img class="pluginlist-inline-logo" src="public/img/grafana_icon.svg"><strong>Pro tip</strong>: To update all plugins at once, type <code class="code--small">grafana-cli plugins update-all</code> on the command line.</div>
 	</div>
 </div>

+ 20 - 0
public/app/features/templating/editorCtrl.js

@@ -20,6 +20,13 @@ function (angular, _) {
       multi: false,
     };
 
+    $scope.variableTypes = [
+      {value: "query",      text: "Query"},
+      {value: "interval",   text: "Interval"},
+      {value: "datasource", text: "Data source"},
+      {value: "custom",     text: "Custom"},
+    ];
+
     $scope.refreshOptions = [
       {value: 0, text: "Never"},
       {value: 1, text: "On Dashboard Load"},
@@ -35,10 +42,16 @@ function (angular, _) {
     $scope.init = function() {
       $scope.mode = 'list';
 
+      $scope.datasourceTypes = {};
       $scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
+        $scope.datasourceTypes[ds.meta.id] = {text: ds.meta.name, value: ds.meta.id};
         return !ds.meta.builtIn;
       });
 
+      $scope.datasourceTypes = _.map($scope.datasourceTypes, function(value) {
+        return value;
+      });
+
       $scope.variables = templateSrv.variables;
       $scope.reset();
 
@@ -132,9 +145,16 @@ function (angular, _) {
       if ($scope.current.type === 'interval') {
         $scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
       }
+
       if ($scope.current.type === 'query') {
         $scope.current.query = '';
       }
+
+      if ($scope.current.type === 'datasource') {
+        $scope.current.query = $scope.datasourceTypes[0].value;
+        $scope.current.regex = '';
+        $scope.current.refresh = 1;
+      }
     };
 
     $scope.removeVariable = function(variable) {

+ 96 - 59
public/app/features/templating/partials/editor.html

@@ -75,39 +75,49 @@
 			<div class="gf-form-group">
 				<div class="gf-form-inline">
 					<div class="gf-form max-width-19">
-						<span class="gf-form-label width-7">Name</span>
-						<input type="text" class="gf-form-input max-width-12" placeholder="name" ng-model='current.name'></input>
-					</div>
-					<div class="gf-form">
-						<span class="gf-form-label width-4">Type</span>
-						<div class="gf-form-select-wrapper">
-							<select class="gf-form-input width-7" ng-model="current.type" ng-options="f for f in ['query', 'interval', 'custom']" ng-change="typeChanged()"></select>
-						</div>
+						<span class="gf-form-label width-6">Name</span>
+						<input type="text" class="gf-form-input" placeholder="name" ng-model='current.name'></input>
 					</div>
 					<div class="gf-form max-width-19">
-						<span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span>
-						<div class="gf-form-select-wrapper max-width-12" ng-show="current.type === 'query'">
-							<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
+						<span class="gf-form-label width-6">
+              Type
+              <info-popover mode="right-normal">
+                <dl>
+                  <dt>Query</dt>
+                  <dd>Variable values are fetched from a metric names query to a data source</dd>
+                  <dt>Interval</dt>
+                  <dd>Timespan variable type</dd>
+                  <dt>Datasource</dt>
+                  <dd>Dynamically switch data sources using this type of variable</dd>
+                  <dt>Custom</dt>
+                  <dd>Define variable values manually</dd>
+                </dl>
+                <a href="http://docs.grafana.org/reference/templating" target="_blank">Templating docs</a>
+              </info-popover>
+            </span>
+						<div class="gf-form-select-wrapper max-width-17">
+							<select class="gf-form-input" ng-model="current.type" ng-options="f.value as f.text for f in variableTypes" ng-change="typeChanged()"></select>
 						</div>
 					</div>
-				</div>
+        </div>
 				<div class="gf-form-inline">
 					<div class="gf-form max-width-19">
-						<span class="gf-form-label width-7">Label</span>
-						<input type="text" class="gf-form-input max-width-12" ng-model='current.label' placeholder="optional display name"></input>
+						<span class="gf-form-label width-6">Label</span>
+						<input type="text" class="gf-form-input" ng-model='current.label' placeholder="optional display name"></input>
 					</div>
-					<div class="gf-form">
-						<span class="gf-form-label width-4">Hide</span>
-            <div class="gf-form-select-wrapper">
-							<select class="gf-form-input width-7" ng-model="current.hide" ng-options="f.value as f.text for f in hideOptions"></select>
+					<div class="gf-form max-width-19">
+						<span class="gf-form-label width-6">Hide</span>
+            <div class="gf-form-select-wrapper max-width-15">
+							<select class="gf-form-input" ng-model="current.hide" ng-options="f.value as f.text for f in hideOptions"></select>
 						</div>
 					</div>
 				</div>
 
 			</div>
 
-			<h5 class="section-heading">Value Options</h5>
 			<div ng-show="current.type === 'interval'" class="gf-form-group">
+        <h5 class="section-heading">Interval Options</h5>
+
 				<div class="gf-form">
 					<span class="gf-form-label width-9">Values</span>
 					<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()"></input>
@@ -135,6 +145,7 @@
 			</div>
 
 			<div ng-show="current.type === 'custom'" class="gf-form-group">
+        <h5 class="section-heading">Custom Options</h5>
 				<div class="gf-form">
 					<span class="gf-form-label width-13">Values seperated by comma</span>
 					<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
@@ -142,43 +153,69 @@
 			</div>
 
 			<div ng-show="current.type === 'query'" class="gf-form-group">
-				<div class="gf-form">
-					<span class="gf-form-label width-7">Query</span>
-					<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()"></input>
-					<!-- <info&#45;popover position="bottom center" wide="true"> -->
-					<!-- 	Example queries: -->
-					<!-- 	<ul> -->
-					<!-- 		<li> -->
-					<!-- 			<code>SHOW TAG VALUES WITH KEY = "hostname"</code> -->
-					<!-- 		</li> -->
-					<!-- 		<li> -->
-					<!-- 			<code>SHOW TAG VALUES WITH KEY = "hostname"</code> -->
-					<!-- 		</li> -->
-					<!-- 		<li> -->
-					<!-- 			<code>SHOW TAG VALUES WITH KEY = "hostname"</code> -->
-					<!-- 		</li> -->
-					<!-- 		<li> -->
-					<!-- 			<a href="http://docs.grafana.org" target="_blank">Templating docs</a> -->
-					<!-- 		</li> -->
-					<!-- 	</ul> -->
-					<!-- </info&#45;popover> -->
-				</div>
-				<div class="gf-form">
-					<span class="gf-form-label width-7">
-						Regex
-						<tip>Optional, if you want to extract part of a series name or metric node segment</tip>
-					</span>
-					<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
-				</div>
-				<div class="gf-form">
-					<span class="gf-form-label width-7">Refresh</span>
-					<select class="gf-form-input max-width-14" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select>
-					<tip>When to update the values of this variable, will slow down dashboard load / time change</tip>
-				</div>
-			</div>
+        <h5 class="section-heading">Query Options</h5>
+
+        <div class="gf-form-inline">
+          <div class="gf-form max-width-21">
+            <span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span>
+            <div class="gf-form-select-wrapper max-width-14">
+              <select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
+            </div>
+          </div>
+          <div class="gf-form max-width-21">
+            <span class="gf-form-label width-7">
+              Refresh
+              <info-popover mode="right-normal">
+                When to update the values of this variable.
+              </info-popover>
+            </span>
+            <div class="gf-form-select-wrapper max-width-14">
+              <select class="gf-form-input" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select>
+            </div>
+          </div>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label width-7">Query</span>
+          <input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()"></input>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label width-7">
+            Regex
+            <info-popover mode="right-normal">
+              Optional, if you want to extract part of a series name or metric node segment.
+            </info-popover>
+          </span>
+          <input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
+        </div>
+      </div>
+
+      <div ng-show="current.type === 'datasource'" class="gf-form-group">
+        <h5 class="section-heading">Data source options</h5>
 
-			<div class="section gf-form-group" >
-				<h5 class="section-heading">Selection Options</h5>
+        <div class="gf-form">
+          <label class="gf-form-label width-12">Type</label>
+          <div class="gf-form-select-wrapper max-width-18">
+            <select class="gf-form-input" ng-model="current.query" ng-options="f.value as f.text for f in datasourceTypes" ng-change="runQuery()"></select>
+          </div>
+        </div>
+
+        <div class="gf-form">
+          <label class="gf-form-label width-12">
+            Instance name filter
+            <info-popover mode="right-normal">
+              Regex filter for which data source instances to choose from in
+              the variable value dropdown. Leave empty for all.
+              <br><br>
+              Example: <code>/^prod/</code>
+
+            </info-popover>
+          </label>
+          <input type="text" class="gf-form-input max-width-18" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
+        </div>
+      </div>
+
+      <div class="section gf-form-group" ng-hide="current.type === 'datasource'">
+        <h5 class="section-heading">Selection Options</h5>
         <div class="section">
           <gf-form-switch class="gf-form"
                           label="Multi-value"
@@ -217,10 +254,10 @@
 
       <div class="gf-form-group">
         <h5>Preview of values (shows max 20)</h5>
-        <div class="gf-form">
-          <span class="gf-form-label" ng-repeat="option in current.options | limitTo: 20">
-            {{option.text}}
-          </span>
+        <div class="gf-form-inline">
+          <div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
+            <span class="gf-form-label">{{option.text}}</span>
+          </div>
         </div>
       </div>
     </div>

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

@@ -57,7 +57,7 @@ function (angular, _) {
           }
 
           var escapedValues = _.map(value, regexEscape);
-          return escapedValues.join('|');
+          return '(' + escapedValues.join('|') + ')';
         }
         case "lucene": {
           if (typeof value === 'string') {
@@ -152,6 +152,10 @@ function (angular, _) {
         value = variable.current.value;
         if (self.isAllValue(value)) {
           value = self.getAllValue(variable);
+          // skip formating of custom all values
+          if (variable.allValue) {
+            return value;
+          }
         }
 
         var res = self.formatValue(value, format, variable);

+ 41 - 10
public/app/features/templating/templateValuesSrv.js

@@ -63,7 +63,9 @@ function (angular, _, kbn) {
       // determine our dependencies.
       if (variable.type === "query") {
         _.forEach(this.variables, function(v) {
-          if (templateSrv.containsVariable(variable.query, v.name)) {
+          // both query and datasource can contain variable
+          if (templateSrv.containsVariable(variable.query, v.name) ||
+              templateSrv.containsVariable(variable.datasource, v.name)) {
             dependencies.push(self.variableLock[v.name].promise);
           }
         });
@@ -149,7 +151,8 @@ function (angular, _, kbn) {
         if (otherVariable === updatedVariable) {
           return;
         }
-        if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name)) {
+        if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name) ||
+            templateSrv.containsVariable(otherVariable.datasource, updatedVariable.name)) {
           return self.updateOptions(otherVariable);
         }
       });
@@ -158,6 +161,11 @@ function (angular, _, kbn) {
     };
 
     this._updateNonQueryVariable = function(variable) {
+      if (variable.type === 'datasource') {
+        self.updateDataSourceVariable(variable);
+        return;
+      }
+
       // extract options in comma seperated string
       variable.options = _.map(variable.query.split(/[,]+/), function(text) {
         return { text: text.trim(), value: text.trim() };
@@ -172,6 +180,36 @@ function (angular, _, kbn) {
       }
     };
 
+    this.updateDataSourceVariable = function(variable) {
+      var options = [];
+      var sources = datasourceSrv.getMetricSources({skipVariables: true});
+      var regex;
+
+      if (variable.regex) {
+        regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex));
+      }
+
+      for (var i = 0; i < sources.length; i++) {
+        var source = sources[i];
+        // must match on type
+        if (source.meta.id !== variable.query) {
+          continue;
+        }
+
+        if (regex && !regex.exec(source.name)) {
+          continue;
+        }
+
+        options.push({text: source.name, value: source.name});
+      }
+
+      if (options.length === 0) {
+        options.push({text: 'No datasurces found', value: ''});
+      }
+
+      variable.options = options;
+    };
+
     this.updateOptions = function(variable) {
       if (variable.type !== 'query') {
         self._updateNonQueryVariable(variable);
@@ -288,17 +326,10 @@ function (angular, _, kbn) {
         options[value] = {text: text, value: value};
       }
 
-      return _.map(_.keys(options).sort(), function(key) {
-        return options[key];
-      });
+      return _.sortBy(options, 'text');
     };
 
     this.addAllOption = function(variable) {
-      if (variable.allValue) {
-        variable.options.unshift({text: 'All', value: variable.allValue});
-        return;
-      }
-
       variable.options.unshift({text: 'All', value: "$__all"});
     };
 

+ 14 - 51
public/app/partials/metrics.html

@@ -1,56 +1,19 @@
-<div class="editor-row">
-
-	<div class="tight-form-container">
-		<div ng-repeat="target in ctrl.panel.targets" ng-class="{'tight-form-disabled': target.hide}">
-			<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
-				<plugin-component type="query-ctrl">
-				</plugin-component>
-			</rebuild-on-change>
-		</div>
-	</div>
-
-	<div style="margin: 20px 0 0 0">
-		<button class="btn btn-inverse" ng-click="ctrl.addDataQuery()" ng-hide="ctrl.datasource.meta.mixed">
-			<i class="fa fa-plus"></i>&nbsp;
-			Query
-		</button>
-
-		<div class="dropdown" ng-if="ctrl.datasource.meta.mixed">
-			<button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
-				<i class="fa fa-plus"></i>&nbsp;
-				Query &nbsp; <span class="caret"></span>
-			</button>
-
-			<ul class="dropdown-menu" role="menu">
-				<li ng-repeat="datasource in ctrl.datasources" role="menuitem" ng-hide="datasource.meta.builtIn">
-					<a ng-click="ctrl.addDataQuery(datasource);">{{datasource.name}}</a>
-				</li>
-			</ul>
-		</div>
-
-	</div>
-
-	<rebuild-on-change property="ctrl.panel.datasource" show-null="true">
-		<plugin-component type="query-options-ctrl">
-		</plugin-component>
-	</rebuild-on-change>
 
+<div class="query-editor-rows gf-form-group">
+  <div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
+    <rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
+      <plugin-component type="query-ctrl">
+      </plugin-component>
+    </rebuild-on-change>
+  </div>
 </div>
 
-<div class="editor-row">
-
-	<div class="pull-right dropdown" style="margin-right: 10px;">
-		<button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown" bs-tooltip="'Datasource'">
-			<i class="fa fa-database"></i>&nbsp;
-			{{ctrl.datasource.name}} &nbsp; <span class="caret"></span>
-		</button>
+<metrics-ds-selector panel-ctrl="ctrl"></metrics-ds-selector>
 
-		<ul class="dropdown-menu" role="menu">
-			<li ng-repeat="datasource in ctrl.datasources" role="menuitem">
-				<a ng-click="ctrl.setDatasource(datasource);">{{datasource.name}}</a>
-			</li>
-		</ul>
-	</div>
-
-	<div class="clearfix"></div>
+<div class="gf-form-group">
+  <rebuild-on-change property="ctrl.panel.datasource" show-null="true">
+    <plugin-component type="query-options-ctrl">
+    </plugin-component>
+  </rebuild-on-change>
 </div>
+

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

@@ -1,22 +1,27 @@
 <h3 class="page-heading">CloudWatch details</h3>
 
-<div class="gf-form-group">
+<div class="gf-form-group max-width-30">
 	<div class="gf-form">
-		<label class="gf-form-label width-14">
-			Credentials profile name<tip>Credentials profile name, as specified in ~/.aws/credentials, leave blank for default</tip>
-		</label>
-		<input type="text" class="gf-form-input max-width-15" ng-model='ctrl.current.database' placeholder="default"></input>
+		<label class="gf-form-label width-13">Credentials profile name</label>
+		<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.database' placeholder="default"></input>
+		<info-popover mode="right-absolute">
+			Credentials profile name, as specified in ~/.aws/credentials, leave blank for default
+		</info-popover>
 	</div>
 	<div class="gf-form">
-		<label class="gf-form-label width-14">
-			Default Region<tip>Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.</tip>
-		</label>
-		<div class="gf-form-select-wrapper">
-			<select class="gf-form-input max-width-15" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select>
+		<label class="gf-form-label width-13">Default Region</label>
+		<div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
+			<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select>
+			<info-popover mode="right-absolute">
+				Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
+			</info-popover>
 		</div>
 	</div>
 	<div class="gf-form">
-		<label class="gf-form-label width-14">Custom Metrics namespace<tip>Namespaces of Custom Metrics</tip></label>
-		<input type="text" class="gf-form-input max-width-15" ng-model='ctrl.current.jsonData.customMetricsNamespaces' placeholder="Namespace1,Namespace2"></input>
+		<label class="gf-form-label width-13">Custom Metrics namespace</label>
+		<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.customMetricsNamespaces' placeholder="Namespace1,Namespace2"></input>
+		<info-popover mode="right-absolute">
+			Namespaces of Custom Metrics
+		</info-popover>
 	</div>
 </div>

+ 2 - 2
public/app/plugins/datasource/cloudwatch/partials/query.editor.html

@@ -1,4 +1,4 @@
-<query-editor-row ctrl="ctrl">
+<query-editor-row query-ctrl="ctrl" can-collapse="false">
+	<cloudwatch-query-parameter target="ctrl.target" datasource="ctrl.datasource" on-change="ctrl.refresh()"></cloudwatch-query-parameter>
 </query-editor-row>
 
-<cloudwatch-query-parameter target="ctrl.target" datasource="ctrl.datasource" on-change="ctrl.refresh()"></cloudwatch-query-parameter>

+ 51 - 50
public/app/plugins/datasource/cloudwatch/partials/query.parameter.html

@@ -1,58 +1,59 @@
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
-			Metric
-		</li>
-		<li>
-			<metric-segment segment="regionSegment" get-options="getRegions()" on-change="regionChanged()"></metric-segment>
-		</li>
-		<li>
-			<metric-segment segment="namespaceSegment" get-options="getNamespaces()" on-change="namespaceChanged()"></metric-segment>
-		</li>
-		<li>
-			<metric-segment segment="metricSegment" get-options="getMetrics()" on-change="metricChanged()"></metric-segment>
-		</li>
-		<li class="tight-form-item query-keyword">
-			Stats
-		</li>
-		<li ng-repeat="segment in statSegments">
-			<metric-segment segment="segment" get-options="getStatSegments(segment, $index)" on-change="statSegmentChanged(segment, $index)"></metric-segment>
-		</li>
-	</ul>
+<div class="gf-form-inline">
+	<div class="gf-form">
+		<label class="gf-form-label query-keyword width-7">Metric</label>
 
-	<div class="clearfix"></div>
+		<metric-segment segment="regionSegment" get-options="getRegions()" on-change="regionChanged()"></metric-segment>
+		<metric-segment segment="namespaceSegment" get-options="getNamespaces()" on-change="namespaceChanged()"></metric-segment>
+		<metric-segment segment="metricSegment" get-options="getMetrics()" on-change="metricChanged()"></metric-segment>
+	</div>
+
+	<div class="gf-form">
+		<label class="gf-form-label query-keyword">Stats</label>
+	</div>
+
+	<div class="gf-form" ng-repeat="segment in statSegments">
+		<metric-segment segment="segment" get-options="getStatSegments(segment, $index)" on-change="statSegmentChanged(segment, $index)"></metric-segment>
+	</div>
+
+	<div class="gf-form gf-form--grow">
+		<div class="gf-form-label gf-form-label--grow"></div>
+	</div>
 </div>
 
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
-			Dimensions
-		</li>
-		<li ng-repeat="segment in dimSegments">
-			<metric-segment segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
-		</li>
-	</ul>
+<div class="gf-form-inline">
+	<div class="gf-form">
+		<label class="gf-form-label query-keyword width-7">Dimensions</label>
+		<metric-segment ng-repeat="segment in dimSegments" segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
+	</div>
 
-	<div class="clearfix"></div>
+	<div class="gf-form gf-form--grow">
+		<div class="gf-form-label gf-form-label--grow"></div>
+	</div>
 </div>
 
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
+<div class="gf-form-inline">
+	<div class="gf-form">
+		<label class="gf-form-label query-keyword width-7">
 			Period
-			<tip>Interval between points in seconds</tip>
-		</li>
-		<li>
-			<input type="text" class="input-mini tight-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="onChange()" />
-		</li>
-		<li class="tight-form-item query-keyword">
-			Alias
-			<tip>{{metric}} {{stat}} {{namespace}} {{region}} {{DIMENSION_NAME}}</tip>
-		</li>
-		<li>
-			<input type="text" class="input-xlarge tight-form-input"  ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="onChange()">
-		</li>
-	</ul>
-	<div class="clearfix"></div>
+			<info-popover mode="right-normal">Interval between points in seconds</info-popover>
+		</label>
+		<input type="text" class="gf-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="onChange()" />
+	</div>
+	<div class="gf-form max-width-30">
+		<label class="gf-form-label query-keyword width-7">Alias</label>
+		<input type="text" class="gf-form-input"  ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="onChange()">
+		<info-popover mode="right-absolute">
+			Alias replacement variables:
+			<ul ng-non-bindable>
+				<li>{{metric}}</li>
+				<li>{{stat}}</li>
+				<li>{{namespace}}</li>
+				<li>{{region}}</li>
+				<li>{{DIMENSION_NAME}}</li>
+			</ul>
+		</info-popover>
+	</div>
+	<div class="gf-form gf-form--grow">
+		<div class="gf-form-label gf-form-label--grow"></div>
+	</div>
 </div>
-

+ 11 - 0
public/app/plugins/datasource/elasticsearch/bucket_agg.js

@@ -60,6 +60,10 @@ function (angular, _, queryDef) {
           $scope.agg.query = '*';
           break;
         }
+        case 'geohash_grid': {
+          $scope.agg.settings.precision = 3;
+          break;
+        }
       }
 
       $scope.validateModel();
@@ -121,6 +125,13 @@ function (angular, _, queryDef) {
           if (settings.trimEdges && settings.trimEdges > 0) {
             settingsLinkText += ', Trim edges: ' + settings.trimEdges;
           }
+          break;
+        }
+        case 'geohash_grid': {
+          // limit precision to 7
+          settings.precision = Math.max(Math.min(settings.precision, 7), 1);
+          settingsLinkText = 'Precision: ' + settings.precision;
+          break;
         }
       }
 

BIN
public/app/plugins/datasource/elasticsearch/img/logo_large.png


+ 69 - 104
public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html

@@ -1,131 +1,96 @@
-<div class="tight-form">
-	<ul class="tight-form-list">
-		<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
+<div class="gf-form-inline">
+	<div class="gf-form">
+		<label class="gf-form-label query-keyword width-7">
 			<span ng-show="isFirst">Group by</span>
 			<span ng-hide="isFirst">Then by</span>
-		</li>
-		<li>
-			<metric-segment-model property="agg.type" options="bucketAggTypes" on-change="onTypeChanged()" custom="false" css-class="tight-form-item-large"></metric-segment-model>
-		</li>
-		<li ng-if="agg.field">
-			<metric-segment-model property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="tight-form-item-xxlarge"></metric-segment>
-		</li>
-		<li ng-if="!agg.field">
-			<span class="tight-form-item tight-form-item-xxlarge">&nbsp;</span>
-		</li>
-		<li class="tight-form-item last" ng-if="settingsLinkText">
+		</label>
+
+		<metric-segment-model property="agg.type" options="bucketAggTypes" on-change="onTypeChanged()" custom="false" css-class="width-10"></metric-segment-model>
+		<metric-segment-model ng-if="agg.field" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model>
+	</div>
+
+	<div class="gf-form gf-form--grow">
+		<label class="gf-form-label gf-form-label--grow">
 			<a ng-click="toggleOptions()">
 				<i class="fa fa-caret-down" ng-show="showOptions"></i>
 				<i class="fa fa-caret-right" ng-hide="showOptions"></i>
 				{{settingsLinkText}}
 			</a>
-		</li>
-	</ul>
+		</label>
+	</div>
 
-	<ul class="tight-form-list pull-right">
-		<li class="tight-form-item last" ng-if="isFirst">
+	<div class="gf-form">
+		<label class="gf-form-label" ng-if="isFirst">
 			<a class="pointer" ng-click="addBucketAgg()"><i class="fa fa-plus"></i></a>
-		</li>
-		<li class="tight-form-item last">
+		</label>
+		<label class="gf-form-label">
 			<a class="pointer" ng-click="removeBucketAgg()"><i class="fa fa-minus"></i></a>
-		</li>
-	</ul>
-	<div class="clearfix"></div>
+		</label>
+	</div>
 </div>
 
-<div class="tight-form" ng-if="showOptions">
-	<div class="tight-form-inner-box" ng-if="agg.type === 'date_histogram'">
-		<div class="tight-form">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 170px">
-					Interval
-				</li>
-				<li>
-					<metric-segment-model property="agg.settings.interval" get-options="getIntervalOptions()" on-change="onChangeInternal()" css-class="last" custom="true"></metric-segment-model>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
+<div class="gf-form-group" ng-if="showOptions">
+	<div ng-if="agg.type === 'date_histogram'">
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label width-10">Interval</label>
+			<metric-segment-model property="agg.settings.interval" get-options="getIntervalOptions()" on-change="onChangeInternal()" css-class="width-12" custom="true"></metric-segment-model>
 		</div>
-		<div class="tight-form">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 170px">
-					Min Doc Count
-				</li>
-				<li>
-					<input type="number" class="tight-form-input" ng-model="agg.settings.min_doc_count" ng-blur="onChangeInternal()">
-				</li>
-			</ul>
-			<div class="clearfix"></div>
+
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label width-10">Min Doc Count</label>
+			<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.min_doc_count" ng-blur="onChangeInternal()">
 		</div>
-		<div class="tight-form last">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 170px">
-					Trim edges points
-				</li>
-				<li>
-					<input class="tight-form-input" type="number" ng-model="agg.settings.trimEdges" ng-change="onChangeInternal()">
-				</li>
-				<li class="tight-form-item last">
-					<i class="fa fa-question-circle" bs-tooltip="'Trim the edges on the timeseries x datapoints'" data-placement="right"></i>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
+
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label width-10">
+				Trim edges
+				<info-popover mode="right-normal">
+					Trim the edges on the timeseries datapoints
+				</info-popover>
+			</label>
+			<input class="gf-form-input max-width-12" type="number" ng-model="agg.settings.trimEdges" ng-change="onChangeInternal()">
 		</div>
 	</div>
-	<div class="tight-form-inner-box" ng-if="agg.type === 'terms'">
-		<div class="tight-form">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 60px">
-					Order
-				</li>
-				<li>
-					<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="last"></metric-segment-model>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
+
+	<div ng-if="agg.type === 'terms'">
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label">Order</label>
+			<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 		</div>
-		<div class="tight-form">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 60px">
-					Size
-				</li>
-				<li>
-					<metric-segment-model property="agg.settings.size" options="sizeOptions" on-change="onChangeInternal()" css-class="last"></metric-segment-model>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
+
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label width-10">Size</label>
+			<metric-segment-model property="agg.settings.size" options="sizeOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 		</div>
-		<div class="tight-form last">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 60px">
-					Order By
-				</li>
-				<li>
-					<metric-segment-model property="agg.settings.orderBy" options="orderByOptions" on-change="onChangeInternal()" css-class="last"></metric-segment-model>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
+
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label width-10">Order By</label>
+			<metric-segment-model property="agg.settings.orderBy" options="orderByOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 		</div>
 	</div>
-	<div class="tight-form-inner-box" ng-if="agg.type === 'filters'">
-		<div class="tight-form" ng-repeat="filter in agg.settings.filters" ng-class="{last: $last}">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 100px">
-					Query {{$index + 1}}
-				</li>
-				<li>
-					<input type="text" class="tight-form-input input-large" ng-model="filter.query" spellcheck='false' placeholder="Lucene query" ng-blur="onChangeInternal()">
-				</li>
-				<li class="tight-form-item last" ng-if="$first">
+
+	<div ng-if="agg.type === 'filters'">
+		<div class="gf-form-inline" ng-repeat="filter in agg.settings.filters" ng-class="{last: $last}">
+			<div class="gf-form">
+				<label class="gf-form-item width-10">Query {{$index + 1}}</label>
+				<input type="text" class="gf-form-input max-width-12" ng-model="filter.query" spellcheck='false' placeholder="Lucene query" ng-blur="onChangeInternal()">
+			</div>
+			<div class="gf-form">
+				<label class="gf-form-label" ng-if="$first">
 					<a class="pointer" ng-click="addFiltersQuery()"><i class="fa fa-plus"></i></a>
-				</li>
-				<li class="tight-form-item last" ng-if="!$first">
+				</label>
+				<label class="gf-form-label" ng-if="!$first">
 					<a class="pointer" ng-click="removeFiltersQuery(filter)"><i class="fa fa-minus"></i></a>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
+				</label>
+			</div>
 		</div>
+	</div>
 
+	<div ng-if="agg.type === 'geohash_grid'">
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label">Precision</label>
+			<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.precision" spellcheck='false' placeholder="3" ng-blur="onChangeInternal()">
+		</div>
 	</div>
 
 </div>

+ 61 - 117
public/app/plugins/datasource/elasticsearch/partials/metric_agg.html

@@ -1,138 +1,82 @@
-<div class="tight-form" ng-class="{'tight-form-disabled': agg.hide}">
-	<ul class="tight-form-list">
-		<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
+<div class="gf-form-inline" ng-class="{'gf-form-disabled': agg.hide}">
+	<div class="gf-form">
+		<label class="gf-form-label query-keyword width-7">
 			Metric
 			&nbsp;
 			<a ng-click="toggleShowMetric()" bs-tooltip="'Click to toggle show / hide metric'">
 				<i class="fa fa-eye" ng-hide="agg.hide"></i>
 				<i class="fa fa-eye-slash" ng-show="agg.hide"></i>
 			</a>
-		</li>
-		<li>
-			<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="tight-form-item-large"></metric-segment-model>
-		</li>
-		<li ng-if="aggDef.requiresField">
-			<metric-segment-model property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="tight-form-item-xxlarge"></metric-segment-model>
-		</li>
-		<li ng-if="aggDef.isPipelineAgg">
-			<metric-segment-model property="agg.pipelineAgg" options="pipelineAggOptions" on-change="onChangeInternal()" custom="false" css-class="tight-form-item-xxlarge"></metric-segment-model>
-		</li>
-		<li class="tight-form-item last" ng-if="settingsLinkText">
-			<a ng-click="toggleOptions()">
+		</label>
+	</div>
+
+	<div class="gf-form">
+		<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="width-10"></metric-segment-model>
+		<metric-segment-model ng-if="aggDef.requiresField" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model>
+		<metric-segment-model ng-if="aggDef.isPipelineAgg" property="agg.pipelineAgg" options="pipelineAggOptions" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
+	</div>
+
+	<div class="gf-form gf-form--grow">
+		<label class="gf-form-label gf-form-label--grow">
+			<a ng-click="toggleOptions()" ng-if="settingsLinkText">
 				<i class="fa fa-caret-down" ng-show="showOptions"></i>
 				<i class="fa fa-caret-right" ng-hide="showOptions"></i>
-				{{settingsLinkText}}
+					{{settingsLinkText}}
 			</a>
-		</li>
-	</ul>
+		</label>
+	</div>
 
-	<ul class="tight-form-list pull-right">
-		<li class="tight-form-item last" ng-if="isFirst">
+	<div class="gf-form">
+		<label class="gf-form-label" ng-if="isFirst">
 			<a class="pointer" ng-click="addMetricAgg()"><i class="fa fa-plus"></i></a>
-		</li>
-		<li class="tight-form-item last" ng-if="!isSingle">
+		</label>
+		<label class="gf-form-label" ng-if="!isSingle">
 			<a class="pointer" ng-click="removeMetricAgg()"><i class="fa fa-minus"></i></a>
-		</li>
-	</ul>
-	<div class="clearfix"></div>
+		</label>
+	</div>
 </div>
 
-<div class="tight-form" ng-if="showOptions">
-	<div class="tight-form-inner-box tight-form-container">
-		<div class="tight-form" ng-if="agg.type === 'derivative'">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 75px;">
-					Unit
-				</li>
-				<li>
-					<input type="text" class="input-medium tight-form-input last" ng-model="agg.settings.unit" ng-blur="onChangeInternal()" spellcheck='false'>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
+<div class="gf-form-group" ng-if="showOptions">
 
-		<div class="tight-form" ng-if="agg.type === 'moving_avg'">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 75px;">
-					Window
-				</li>
-				<li>
-					<input type="number" class="input-medium tight-form-input last" ng-model="agg.settings.window" ng-blur="onChangeInternal()" spellcheck='false'>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
-		<div class="tight-form" ng-if="agg.type === 'moving_avg'">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 75px;">
-					Model
-				</li>
-				<li>
-					<input type="text" class="input-medium tight-form-input last" ng-change="onChangeInternal()" ng-model="agg.settings.model" blur="onChange()" spellcheck='false'>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
-		<div class="tight-form last" ng-if="agg.type === 'percentiles'">
-			<ul class="tight-form-list">
-				<li class="tight-form-item">
-					Percentiles
-				</li>
-				<li>
-					<input type="text" class="input-xlarge tight-form-input last" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
-		<div ng-if="agg.type === 'extended_stats'">
-			<div class="tight-form" ng-repeat="stat in extendedStats">
-				<ul class="tight-form-list">
-					<li class="tight-form-item" style="width: 100px">
-						{{stat.text}}
-					</li>
-					<li class="tight-form-item last">
-						<editor-checkbox text="" model="agg.meta.{{stat.value}}" change="onChange()"></editor-checkbox>
-					</li>
-				</ul>
-				<div class="clearfix"></div>
-			</div>
-		</div>
-		<div class="tight-form" ng-if="agg.type === 'extended_stats'">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 100px">
-					Sigma
-				</li>
-				<li>
-					<input type="number" class="input-mini tight-form-input last" placeholder="3" ng-model="agg.settings.sigma" ng-blur="onChange()"></input>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
+	<div class="gf-form offset-width-7" ng-if="agg.type === 'derivative'">
+		<label class="gf-form-label width-10">Unit</label>
+		<input type="text" class="gf-form-input max-width-12" ng-model="agg.settings.unit" ng-blur="onChangeInternal()" spellcheck='false'>
+	</div>
 
-		<div class="tight-form" ng-if="aggDef.supportsInlineScript">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 100px;">
-					Script
-				</li>
-				<li>
-					<input type="text" class="input-medium tight-form-input last" empty-to-null ng-model="agg.inlineScript" ng-blur="onChangeInternal()" spellcheck='false' placeholder="_value * 1">
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
+	<div class="gf-form offset-width-7" ng-if="agg.type === 'moving_avg'">
+		<label class="gf-form-label width-10">Window</label>
+		<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.window" ng-blur="onChangeInternal()" spellcheck='false'>
+	</div>
 
-		<div class="tight-form" ng-if="aggDef.supportsMissing">
-			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 100px;">
-					Missing
-					<tip>The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value</tip>
-				</li>
-				<li>
-					<input type="number" class="input-medium tight-form-input last" empty-to-null ng-model="agg.settings.missing" ng-blur="onChangeInternal()" spellcheck='false'>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
+	<div class="gf-form offset-width-7" ng-if="agg.type === 'moving_avg'">
+		<label class="gf-form-label width-10">Model</label>
+		<input type="text" class="gf-form-input max-width-12" ng-change="onChangeInternal()" ng-model="agg.settings.model" blur="onChange()" spellcheck='false'>
+	</div>
+
+	<div class="gf-form offset-width-7" ng-if="agg.type === 'percentiles'">
+		<label class="gf-form-label width-10">Percentiles</label>
+		<input type="text" class="gf-form-input max-width-12" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>
+	</div>
+
+	<div ng-if="agg.type === 'extended_stats'">
+		<gf-form-switch ng-repeat="stat in extendedStats" class="gf-form offset-width-7" label="{{stat.text}}" label-class="width-10" checked="agg.meta[stat.value]" on-change="onChangeInternal()"></gf-form-switch>
+
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label width-10">Sigma</label>
+			<input type="number" class="gf-form-input max-width-12" placeholder="3" ng-model="agg.settings.sigma" ng-blur="onChange()"></input>
 		</div>
+	</div>
+
+	<div class="gf-form offset-width-7" ng-if="aggDef.supportsInlineScript">
+		<label class="gf-form-label width-10">Script</label>
+		<input type="text" class="gf-form-input max-width-12" empty-to-null ng-model="agg.inlineScript" ng-blur="onChangeInternal()" spellcheck='false' placeholder="_value * 1">
+	</div>
 
+	<div class="gf-form offset-width-7" ng-if="aggDef.supportsMissing">
+		<label class="gf-form-label width-10">
+			Missing
+			<tip>The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value</tip>
+		</label>
+		<input type="number" class="gf-form-input max-width-12" empty-to-null ng-model="agg.settings.missing" ng-blur="onChangeInternal()" spellcheck='false'>
 	</div>
 </div>

+ 28 - 29
public/app/plugins/datasource/elasticsearch/partials/query.editor.html

@@ -1,32 +1,31 @@
-<query-editor-row ctrl="ctrl">
-	<li class="tight-form-item query-keyword" style="width: 75px">
-		Query
-	</li>
-	<li>
-		<input type="text" class="tight-form-input" style="width: 345px;" ng-model="ctrl.target.query" spellcheck='false' placeholder="Lucene query" ng-blur="ctrl.refresh()">
-	</li>
-	<li class="tight-form-item query-keyword">
-		Alias
-	</li>
-	<li>
-		<input type="text" class="tight-form-input" style="width: 200px;" ng-model="ctrl.target.alias" spellcheck='false' placeholder="alias patterns (empty = auto)" ng-blur="ctrl.refresh()">
-	</li>
-</query-editor-row>
+<query-editor-row query-ctrl="ctrl" can-collapse="true">
+
+	<div class="gf-form-inline">
+		<div class="gf-form gf-form--grow">
+			<label class="gf-form-label query-keyword width-7">Query</label>
+			<input type="text" class="gf-form-input" ng-model="ctrl.target.query" spellcheck='false' placeholder="Lucene query" ng-blur="ctrl.refresh()">
+		</div>
+		<div class="gf-form max-width-15">
+			<label class="gf-form-label query-keyword">Alias</label>
+			<input type="text" class="gf-form-input" ng-model="ctrl.target.alias" spellcheck='false' placeholder="alias patterns" ng-blur="ctrl.refresh()">
+		</div>
+	</div>
 
-<div ng-repeat="agg in ctrl.target.metrics">
-	<elastic-metric-agg
-		target="ctrl.target" index="$index"
-		get-fields="ctrl.getFields($fieldType)"
-		on-change="ctrl.queryUpdated()"
-		es-version="ctrl.esVersion">
-	</elastic-metric-agg>
-</div>
+	<div ng-repeat="agg in ctrl.target.metrics">
+		<elastic-metric-agg
+			target="ctrl.target" index="$index"
+			get-fields="ctrl.getFields($fieldType)"
+			on-change="ctrl.queryUpdated()"
+			es-version="ctrl.esVersion">
+		</elastic-metric-agg>
+	</div>
 
-<div ng-repeat="agg in ctrl.target.bucketAggs">
-	<elastic-bucket-agg
-		target="ctrl.target" index="$index"
-		get-fields="ctrl.getFields($fieldType)"
-		on-change="ctrl.queryUpdated()">
-	</elastic-bucket-agg>
-</div>
+	<div ng-repeat="agg in ctrl.target.bucketAggs">
+		<elastic-bucket-agg
+			target="ctrl.target" index="$index"
+			get-fields="ctrl.getFields($fieldType)"
+			on-change="ctrl.queryUpdated()">
+		</elastic-bucket-agg>
+	</div>
 
+</query-editor-row>

+ 4 - 0
public/app/plugins/datasource/elasticsearch/query_builder.js

@@ -153,6 +153,10 @@ function (queryDef) {
           this.buildTermsAgg(aggDef, esAgg, target);
           break;
         }
+        case 'geohash_grid': {
+          esAgg['geohash_grid'] = {field: aggDef.field, precision: aggDef.settings.precision};
+          break;
+        }
       }
 
       nestedAggs.aggs = nestedAggs.aggs || {};

+ 43 - 0
public/app/plugins/datasource/elasticsearch/query_ctrl.ts

@@ -5,6 +5,7 @@ import './metric_agg';
 
 import angular from 'angular';
 import _ from 'lodash';
+import queryDef from './query_def';
 import {QueryCtrl} from 'app/plugins/sdk';
 
 export class ElasticQueryCtrl extends QueryCtrl {
@@ -38,6 +39,48 @@ export class ElasticQueryCtrl extends QueryCtrl {
     this.$rootScope.appEvent('elastic-query-updated');
   }
 
+  getCollapsedText() {
+    var metricAggs = this.target.metrics;
+    var bucketAggs = this.target.bucketAggs;
+    var metricAggTypes = queryDef.getMetricAggTypes(this.esVersion);
+    var bucketAggTypes = queryDef.bucketAggTypes;
+    var text = '';
+
+    if (this.target.query) {
+      text += 'Query: ' + this.target.query + ', ';
+    }
+
+    text += 'Metrics: ';
+
+    _.each(metricAggs, (metric, index) => {
+      var aggDef = _.findWhere(metricAggTypes, {value: metric.type});
+      text += aggDef.text + '(';
+      if (aggDef.requiresField) {
+        text += metric.field;
+      }
+      text += '), ';
+    });
+
+    _.each(bucketAggs, (bucketAgg, index) => {
+      if (index === 0) {
+        text += ' Group by: ';
+      }
+
+      var aggDef = _.findWhere(bucketAggTypes, {value: bucketAgg.type});
+      text += aggDef.text + '(';
+      if (aggDef.requiresField) {
+        text += bucketAgg.field;
+      }
+      text += '), ';
+    });
+
+    if (this.target.alias) {
+      text += 'Alias: ' + this.target.alias;
+    }
+
+    return text;
+  }
+
   handleQueryError(err) {
     this.error = err.message || 'Failed to issue metric query';
     return [];

+ 3 - 2
public/app/plugins/datasource/elasticsearch/query_def.js

@@ -20,9 +20,10 @@ function (_) {
     ],
 
     bucketAggTypes: [
-      {text: "Terms",           value: 'terms' },
+      {text: "Terms",           value: 'terms', requiresField: true},
       {text: "Filters",         value: 'filters' },
-      {text: "Date Histogram",  value: 'date_histogram' },
+      {text: "Geo Hash Grid",        value: 'geohash_grid', requiresField: true},
+      {text: "Date Histogram",  value: 'date_histogram', requiresField: true},
     ],
 
     orderByOptions: [

+ 6 - 4
public/app/plugins/datasource/grafana/partials/query.editor.html

@@ -1,5 +1,7 @@
-<query-editor-row ctrl="ctrl">
-	<li class="tight-form-item">
-		Test metric (fake data source)
-	</li>
+<query-editor-row query-ctrl="ctrl" can-collapse="false">
+	<div class="gf-form-inline">
+		<div class="gf-form">
+			<label class="gf-form-label">Test metric (fake data source)</label>
+		</div>
+	</div>
 </query-editor-row>

+ 2 - 2
public/app/plugins/datasource/graphite/add_graphite_func.js

@@ -11,10 +11,10 @@ function (angular, _, $, gfunc) {
     .module('grafana.directives')
     .directive('graphiteAddFunc', function($compile) {
       var inputTemplate = '<input type="text"'+
-                            ' class="tight-form-input input-medium tight-form-input"' +
+                            ' class="gf-form-input"' +
                             ' spellcheck="false" style="display:none"></input>';
 
-      var buttonTemplate = '<a  class="tight-form-item tight-form-func dropdown-toggle"' +
+      var buttonTemplate = '<a  class="gf-form-label query-part dropdown-toggle"' +
                               ' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
                               '<i class="fa fa-plus"></i></a>';
 

+ 7 - 0
public/app/plugins/datasource/graphite/gfunc.js

@@ -80,6 +80,13 @@ function (_, $) {
     category: categories.Calculate,
   });
 
+  addFuncDef({
+    name: 'stddevSeries',
+    params: optionalSeriesRefArgs,
+    defaultParams: [''],
+    category: categories.Calculate,
+  });
+
   addFuncDef({
     name: 'divideSeries',
     params: optionalSeriesRefArgs,

+ 21 - 15
public/app/plugins/datasource/graphite/partials/query.editor.html

@@ -1,21 +1,27 @@
-<query-editor-row ctrl="ctrl">
+<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
 
-	<li class="tight-form-flex-wrapper" ng-show="ctrl.target.textEditor">
-		<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="ctrl.target.target" give-focus="ctrl.target.textEditor" spellcheck='false' ng-model-onblur ng-change="ctrl.targetTextChanged()"></input>
-	</li>
+	<div class="gf-form" ng-show="ctrl.target.textEditor">
+		<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.refresh()"></input>
+	</div>
 
-	<li ng-hide-start="ctrl.target.textEditor"></li>
+  <div ng-hide="ctrl.target.textEditor">
+		<div class="gf-form-inline">
+      <div ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
+        <metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
+      </div>
 
-	<li ng-repeat="segment in ctrl.segments" role="menuitem">
-		<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
-	</li>
-	<li ng-repeat="func in ctrl.functions">
-		<span graphite-func-editor class="tight-form-item tight-form-func">
-		</span>
-	</li>
-	<li class="dropdown" graphite-add-func>
-	</li>
+      <div ng-repeat="func in ctrl.functions" class="gf-form">
+        <span graphite-func-editor class="gf-form-label query-part"></span>
+      </div>
 
-	<li ng-hide-end></li>
+      <div class="gf-form dropdown">
+        <span graphite-add-func></span>
+      </div>
+
+      <div class="gf-form gf-form--grow">
+				<div class="gf-form-label gf-form-label--grow"></div>
+			</div>
+    </div>
+  </div>
 
 </query-editor-row>

+ 2 - 2
public/app/plugins/datasource/influxdb/README.md

@@ -6,8 +6,8 @@ There are currently two separate datasources for InfluxDB in Grafana: InfluxDB 0
 
 This is the plugin for InfluxDB 0.9. It is rapidly evolving and we continue to track its API. 
 
-InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users. You can find it [here](https://www.grafana.net/plugins/grafana-influxdb-08-datasource).
+InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users. You can find it [here](https://grafana.net/plugins/grafana-influxdb-08-datasource).
 
 Read more about InfluxDB here:
 
-[http://docs.grafana.org/datasources/influxdb/](http://docs.grafana.org/datasources/influxdb/)
+[http://docs.grafana.org/datasources/influxdb/](http://docs.grafana.org/datasources/influxdb/)

+ 8 - 8
public/app/plugins/datasource/influxdb/influx_query.ts

@@ -152,7 +152,9 @@ export default class InfluxQuery {
       if (interpolate) {
         value = this.templateSrv.replace(value, this.scopedVars);
       }
-      value = "'" + value.replace('\\', '\\\\') + "'";
+      if (isNaN(+value)) {
+        value = "'" + value.replace('\\', '\\\\') + "'";
+      }
     } else if (interpolate){
       value = this.templateSrv.replace(value, this.scopedVars, 'regex');
     }
@@ -160,12 +162,14 @@ export default class InfluxQuery {
     return str + '"' + tag.key + '" ' + operator + ' ' + value;
   }
 
-  getMeasurementAndPolicy() {
+  getMeasurementAndPolicy(interpolate) {
     var policy = this.target.policy;
-    var measurement = this.target.measurement;
+    var measurement = this.target.measurement || 'measurement';
 
     if (!measurement.match('^/.*/')) {
       measurement = '"' + measurement+ '"';
+    } else if (interpolate) {
+      measurement = this.templateSrv.replace(measurement, this.scopedVars, 'regex');
     }
 
     if (policy !== 'default') {
@@ -188,10 +192,6 @@ export default class InfluxQuery {
       }
     }
 
-    if (!target.measurement) {
-      throw {message: "Metric measurement is missing"};
-    }
-
     var query = 'SELECT ';
     var i, y;
     for (i = 0; i < this.selectModels.length; i++) {
@@ -208,7 +208,7 @@ export default class InfluxQuery {
       query += selectText;
     }
 
-    query += ' FROM ' + this.getMeasurementAndPolicy() + ' WHERE ';
+    query += ' FROM ' + this.getMeasurementAndPolicy(interpolate) + ' WHERE ';
     var conditions = _.map(target.tags, (tag, index) => {
       return this.renderTagCondition(tag, index, interpolate);
     });

+ 86 - 63
public/app/plugins/datasource/influxdb/partials/query.editor.html

@@ -1,73 +1,96 @@
-<query-editor-row ctrl="ctrl">
-		<ul class="tight-form-list" ng-hide="ctrl.target.rawQuery">
-			<li class="tight-form-item query-keyword" style="width: 75px">
-				FROM
-			</li>
-			<li>
+<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="true">
+
+	<div class="gf-form" ng-if="ctrl.target.rawQuery">
+		<input type="text" class="gf-form-input" ng-model="ctrl.target.query" spellcheck="false" ng-blur="ctrl.refresh()"></input>
+	</div>
+
+	<div ng-if="!ctrl.target.rawQuery">
+
+		<div class="gf-form-inline">
+			<div class="gf-form">
+				<label class="gf-form-label query-keyword width-7">FROM</label>
+
 				<metric-segment segment="ctrl.policySegment" get-options="ctrl.getPolicySegments()" on-change="ctrl.policyChanged()"></metric-segment>
-			</li>
-			<li>
 				<metric-segment segment="ctrl.measurementSegment" get-options="ctrl.getMeasurements()" on-change="ctrl.measurementChanged()"></metric-segment>
-			</li>
-			<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
-				WHERE
-			</li>
-			<li ng-repeat="segment in ctrl.tagSegments">
+			</div>
+
+			<div class="gf-form">
+				<label class="gf-form-label query-keyword">WHERE</label>
+			</div>
+
+			<div class="gf-form" ng-repeat="segment in ctrl.tagSegments">
 				<metric-segment segment="segment" get-options="ctrl.getTagsOrValues(segment, $index)" on-change="ctrl.tagSegmentUpdated(segment, $index)"></metric-segment>
-			</li>
-		</ul>
+			</div>
 
-		<div class="tight-form-flex-wrapper" ng-show="ctrl.target.rawQuery">
-			<input type="text" class="tight-form-clear-input" ng-model="ctrl.target.query" spellcheck="false" style="width: 100%;" ng-blur="ctrl.refresh()"></input>
+			<div class="gf-form gf-form--grow">
+				<div class="gf-form-label gf-form-label--grow"></div>
+			</div>
+		</div>
+
+		<div class="gf-form-inline" ng-repeat="selectParts in ctrl.queryModel.selectModels">
+			<div class="gf-form">
+				<label class="gf-form-label query-keyword width-7">
+					<span ng-show="$index === 0">SELECT</span>
+				</label>
+			</div>
+
+			<div class="gf-form" ng-repeat="part in selectParts">
+				<influx-query-part-editor
+														class="gf-form-label query-part"
+														part="part"
+														remove-action="ctrl.removeSelectPart(selectParts, part)"
+														part-updated="ctrl.selectPartUpdated(selectParts, part)"
+														get-options="ctrl.getPartOptions(part)">
+				</influx-query-part-editor>
+			</div>
+
+			<div class="gf-form">
+				<label class="dropdown"
+								dropdown-typeahead="ctrl.selectMenu"
+								dropdown-typeahead-on-select="ctrl.addSelectPart(selectParts, $item, $subItem)">
+				</label>
+			</div>
+
+			<div class="gf-form gf-form--grow">
+				<div class="gf-form-label gf-form-label--grow"></div>
+			</div>
+		</div>
+
+		<div class="gf-form-inline">
+			<div class="gf-form">
+				<label class="gf-form-label query-keyword width-7">
+					<span>GROUP BY</span>
+				</label>
+
+				<influx-query-part-editor
+								ng-repeat="part in ctrl.queryModel.groupByParts"
+								part="part"
+								class="gf-form-label query-part"
+								remove-action="ctrl.removeGroupByPart(part, $index)" part-updated="ctrl.refresh();" get-options="ctrl.getPartOptions(part)">
+				</influx-query-part-editor>
+			</div>
+
+			<div class="gf-form gf-form--grow">
+				<div class="gf-form-label gf-form-label--grow"></div>
+			</div>
 		</div>
-</query-editor-row>
 
-<div ng-hide="ctrl.target.rawQuery">
-	<div class="tight-form" ng-repeat="selectParts in ctrl.queryModel.selectModels">
-		<ul class="tight-form-list">
-			<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
-				<span ng-show="$index === 0">SELECT</span>
-			</li>
-			<li ng-repeat="part in selectParts">
-				<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="ctrl.removeSelectPart(selectParts, part)" part-updated="ctrl.selectPartUpdated(selectParts, part)" get-options="ctrl.getPartOptions(part)"></influx-query-part-editor>
-			</li>
-			<li class="dropdown" dropdown-typeahead="ctrl.selectMenu" dropdown-typeahead-on-select="ctrl.addSelectPart(selectParts, $item, $subItem)">
-			</li>
-		</ul>
-		<div class="clearfix"></div>
 	</div>
 
-	<div class="tight-form">
-		<ul class="tight-form-list">
-			<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
-				<span>GROUP BY</span>
-			</li>
-			<li ng-repeat="part in ctrl.queryModel.groupByParts">
-				<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="ctrl.removeGroupByPart(part, $index)" part-updated="ctrl.refresh();" get-options="ctrl.getPartOptions(part)"></influx-query-part-editor>
-			</li>
-			<li>
-				<metric-segment segment="ctrl.groupBySegment" get-options="ctrl.getGroupByOptions()" on-change="ctrl.groupByAction(part, $index)"></metric-segment>
-			</li>
-		</ul>
-		<div class="clearfix"></div>
+	<div class="gf-form-inline">
+		<div class="gf-form max-width-30">
+			<label class="gf-form-label query-keyword width-7">ALIAS BY</label>
+			<input type="text" class="gf-form-input" ng-model="ctrl.target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="ctrl.refresh()">
+		</div>
+		<div class="gf-form">
+			<label class="gf-form-label">Format as</label>
+			<div class="gf-form-select-wrapper">
+				<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
+			</div>
+		</div>
+		<div class="gf-form gf-form--grow">
+			<div class="gf-form-label gf-form-label--grow"></div>
+		</div>
 	</div>
-</div>
-
-<div class="tight-form">
-	<ul class="tight-form-list">
-		<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
-			ALIAS BY
-		</li>
-		<li>
-			<input type="text" class="tight-form-clear-input input-xlarge" ng-model="ctrl.target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="ctrl.refresh()">
-		</li>
-		<li class="tight-form-item">
-			Format as
-		</li>
-		<li>
-			<select class="input-small tight-form-input" style="width: 104px" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
-		</li>
-	</ul>
-	<div class="clearfix"></div>
-</div>
 
+</query-editor-row>

+ 1 - 1
public/app/plugins/datasource/influxdb/partials/query.options.html

@@ -38,7 +38,7 @@
 </section>
 
 <div class="editor-row">
-	<div class="pull-left" style="margin-top: 30px;">
+	<div class="pull-left">
 
 		<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
 			<h5>Alias patterns</h5>

+ 1 - 1
public/app/plugins/datasource/influxdb/partials/query_part.html

@@ -2,4 +2,4 @@
 	<span class="pointer fa fa-remove" ng-click="removeActionInternal()" ></span>
 </div>
 
-<a ng-click="toggleControls()">{{part.def.type}}</a><span>(</span><span class="query-part-parameters"></span><span>)</span>
+<a ng-click="toggleControls()" class="query-part-name">{{part.def.type}}</a><span>(</span><span class="query-part-parameters"></span><span>)</span>

+ 2 - 2
public/app/plugins/datasource/influxdb/query_builder.js

@@ -25,8 +25,8 @@ function (_) {
       }
     }
 
-    // quote value unless regex
-    if (operator !== '=~' && operator !== '!~') {
+    // quote value unless regex or number
+    if (operator !== '=~' && operator !== '!~' && isNaN(+value)) {
       value = "'" + value + "'";
     }
 

+ 10 - 1
public/app/plugins/datasource/influxdb/query_ctrl.ts

@@ -23,6 +23,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
   measurementSegment: any;
   removeTagFilterSegment: any;
 
+
   /** @ngInject **/
   constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) {
     super($scope, $injector);
@@ -154,7 +155,11 @@ export class InfluxQueryCtrl extends QueryCtrl {
   }
 
   toggleEditorMode() {
-    this.target.query = this.queryModel.render(false);
+    try {
+      this.target.query = this.queryModel.render(false);
+    } catch (err) {
+      console.log('query render error');
+    }
     this.target.rawQuery = !this.target.rawQuery;
   }
 
@@ -316,5 +321,9 @@ export class InfluxQueryCtrl extends QueryCtrl {
       return '=';
     }
   }
+
+  getCollapsedText() {
+    return this.queryModel.render(false);
+  }
 }
 

+ 21 - 9
public/app/plugins/datasource/influxdb/response_parser.ts

@@ -12,17 +12,29 @@ export default class ResponseParser {
       return [];
     }
 
-    var series = influxResults.series[0];
-    return _.map(series.values, (value) => {
-      if (_.isArray(value)) {
-        if (query.toLowerCase().indexOf('show tag values') >= 0) {
-          return { text: (value[1] || value[0]) };
+    var influxdb11format = query.toLowerCase().indexOf('show tag values') >= 0;
+
+    var res = {};
+    _.each(influxResults.series, serie => {
+      _.each(serie.values, value => {
+        if (_.isArray(value)) {
+          if (influxdb11format) {
+            addUnique(res, value[1] || value[0]);
+          } else {
+            addUnique(res, value[0]);
+          }
         } else {
-          return { text: value[0] };
+          addUnique(res, value);
         }
-      } else {
-        return { text: value };
-      }
+      });
+    });
+
+    return _.map(res, value => {
+      return { text: value};
     });
   }
 }
+
+function addUnique(arr, value) {
+  arr[value] = value;
+}

+ 17 - 9
public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts

@@ -38,7 +38,7 @@ describe("influxdb response parser", () => {
               {
                 "name": "hostnameTagValues",
                 "columns": ["hostname"],
-                "values": [ ["server1"], ["server2"] ]
+                "values": [ ["server1"], ["server2"], ["server2"] ]
               }
             ]
           }
@@ -54,7 +54,7 @@ describe("influxdb response parser", () => {
       });
     });
 
-    describe("response from 0.11.0", () => {
+    describe("response from 0.12.0", () => {
       var response = {
         "results": [
            {
@@ -62,8 +62,19 @@ describe("influxdb response parser", () => {
                {
                  "name": "cpu",
                  "columns": [ "key", "value"],
-                 "values": [ [ "source", "site" ], [ "source", "api" ] ]
-               }
+                 "values": [
+                   [ "source", "site" ],
+                   [ "source", "api" ]
+                 ]
+               },
+               {
+                 "name": "logins",
+                 "columns": [ "key", "value"],
+                 "values": [
+                   [ "source", "site" ],
+                   [ "source", "webapi"]
+                 ]
+               },
              ]
            }
         ]
@@ -72,15 +83,12 @@ describe("influxdb response parser", () => {
       var result = this.parser.parse(query, response);
 
       it("should get two responses", () => {
-        expect(_.size(result)).to.be(2);
+        expect(_.size(result)).to.be(3);
         expect(result[0].text).to.be('site');
         expect(result[1].text).to.be('api');
+        expect(result[2].text).to.be('webapi');
       });
     });
-
-
-
-
   });
 
   describe("SHOW FIELD response", () => {

+ 250 - 220
public/app/plugins/datasource/opentsdb/partials/query.editor.html

@@ -1,223 +1,253 @@
-<query-editor-row ctrl="ctrl">
-	<li class="tight-form-item query-keyword" style="width: 100px">
-		Metric
-	</li>
-	<li>
-		<input type="text" class="input-large tight-form-input" ng-model="ctrl.target.metric"
-		spellcheck='false' bs-typeahead="ctrl.suggestMetrics" placeholder="metric name" data-min-length=0 data-items=100
-		ng-blur="ctrl.targetBlur()">
-		</input>
-		<a bs-tooltip="ctrl.errors.metric" style="color: rgb(229, 189, 28)" ng-show="ctrl.errors.metric">
-			<i class="fa fa-warning"></i>
-		</a>
-	</li>
-	<li class="tight-form-item query-keyword">
-		Aggregator
-	</li>
-	<li>
-		<select ng-model="ctrl.target.aggregator" class="tight-form-input input-small"
-			ng-options="agg for agg in ctrl.aggregators"
-			ng-change="ctrl.targetBlur()">
-		</select>
-		<a bs-tooltip="ctrl.errors.aggregator" style="color: rgb(229, 189, 28)" ng-show="ctrl.errors.aggregator">
-			<i class="fa fa-warning"></i>
-		</a>
-	</li>
-
-	<li class="tight-form-item query-keyword">
-		Alias:
-		<tip>Use patterns like $tag_tagname to replace part of the alias for a tag value</tip>
-	</li>
-	<li>
-		<input type="text" class="tight-form-input input-large"
-		ng-model="ctrl.target.alias"
-		spellcheck='false'
-		placeholder="series alias"
-		data-min-length=0 data-items=100
-		ng-blur="ctrl.targetBlur()"></input>
-	</li>
-</query-editor-row>
+<query-editor-row query-ctrl="ctrl" can-collapse="false">
+	<div class="gf-form-inline">
+		<div class="gf-form max-width-25">
+			<label class="gf-form-label query-keyword width-8">
+				Metric
+				<label class="gf-form-label" bs-tooltip="ctrl.errors.metric" style="color: rgb(229, 189, 28)" ng-show="ctrl.errors.metric">
+					<i class="fa fa-warning"></i>
+				</label>
+			</label>
+			<input type="text" class="gf-form-input" ng-model="ctrl.target.metric"
+						 spellcheck='false' bs-typeahead="ctrl.suggestMetrics" placeholder="metric name" data-min-length=0 data-items=100
+						ng-blur="ctrl.targetBlur()">
+			</input>
+		</div>
+		<div class="gf-form">
+			<label class="gf-form-label query-keyword">
+				Aggregator
+				<a bs-tooltip="ctrl.errors.aggregator" style="color: rgb(229, 189, 28)" ng-show="ctrl.errors.aggregator">
+					<i class="fa fa-warning"></i>
+				</a>
+			</label>
+			<div class="gf-form-select-wrapper max-width-15">
+				<select ng-model="ctrl.target.aggregator" class="gf-form-input"
+								ng-options="agg for agg in ctrl.aggregators"
+								ng-change="ctrl.targetBlur()">
+		 	 </select>
+			</div>
+		</div>
+			<div class="gf-form max-width-20">
+				<label class="gf-form-label query-keyword width-6">
+					Alias:
+					<info-popover mode="right-normal">
+						Use patterns like $tag_tagname to replace part of the alias for a tag value
+					</info-popover>
+				</label>
+				<input  type="text" class="gf-form-input"
+		   					ng-model="ctrl.target.alias"
+								spellcheck='false'
+								placeholder="series alias"
+								data-min-length=0 data-items=100
+								ng-blur="ctrl.targetBlur()"></input>
+			</div>
+
+			<div class="gf-form gf-form--grow">
+				<div class="gf-form-label gf-form-label--grow"></div>
+			</div>
+	</div>
+
+	<div class="gf-form-inline">
+		<div class="gf-form max-width-25">
+			<label class="gf-form-label query-keyword width-8">Down sample</label>
+			<input type="text" class="gf-form-input"
+						 ng-model="ctrl.target.downsampleInterval"
+						 ng-model-onblur
+			       ng-change="ctrl.targetBlur()"
+			       placeholder="interval"></input>
+			<info-popover mode="right-absolute">
+				blank for auto, or for example <code>1m</code>
+			</info-popover>
+		</div>
+
+		<div class="gf-form">
+			<label class="gf-form-label query-keyword">Aggregator</label>
+			<div class="gf-form-select-wrapper">
+				<select ng-model="ctrl.target.downsampleAggregator" class="gf-form-input"
+								ng-options="agg for agg in ctrl.aggregators"
+				        ng-change="ctrl.targetBlur()">
+				</select>
+			</div>
+		</div>
+
+		<div class="gf-form" ng-if="ctrl.tsdbVersion == 2">
+			<label class="gf-form-label query-keyword width-6">Fill</label>
+			<div class="gf-form-select-wrapper">
+				<select ng-model="ctrl.target.downsampleFillPolicy" class="gf-form-input"
+								ng-options="agg for agg in ctrl.fillPolicies"
+								ng-change="ctrl.targetBlur()">
+				</select>
+			</div>
+		</div>
+
+		<gf-form-switch class="gf-form"
+										label="Disable downsampling"
+										checked="ctrl.target.disableDownsampling"
+										on-change="ctrl.targetBlur()">
+		</gf-form-switch>
+
+		<div class="gf-form gf-form--grow">
+			<div class="gf-form-label gf-form-label--grow"></div>
+		</div>
+	</div>
+
+	<div class="gf-form-inline" ng-if="ctrl.tsdbVersion == 2">
+		<div class="gf-form">
+
+			<label class="gf-form-label query-keyword width-8">
+				Filters
+				<info-popover mode="right-normal">
+					Filters does not work with tags, either of the two will work but not both.
+				</info-popover>
+			</label>
+
+			<div ng-repeat="fil in ctrl.target.filters track by $index" class="gf-form-label">
+				{{fil.tagk}}&nbsp;=&nbsp;{{fil.type}}&#40;{{fil.filter}}&#41;&nbsp;&#44&nbsp;groupBy&nbsp;=&nbsp;{{fil.groupBy}}
+				<a ng-click="ctrl.editFilter(fil, $index)">
+					<i class="fa fa-pencil"></i>
+				</a>
+				<a ng-click="ctrl.removeFilter($index)">
+					<i class="fa fa-remove"></i>
+				</a>
+			</div>
+			<label class="gf-form-label query-keyword" ng-hide="ctrl.addFilterMode">
+				<a ng-click="ctrl.addFilter()">
+					<i class="fa fa-plus"></i>
+				</a>
+			</label>
+ 		</div>
+
+		<div class="gf-form-inline" ng-show="ctrl.addFilterMode">
+			<div class="gf-form">
+				<input type="text" class="gf-form-input" spellcheck='false'
+						 bs-typeahead="ctrl.suggestTagKeys" data-min-length=0 data-items=100
+             ng-model="ctrl.target.currentFilterKey" placeholder="key">
+				</input>
+			</div>
+
+			<div class="gf-form">
+				<label class="gf-form-label">Type</label>
+				<div class="gf-form-select-wrapper">
+					<select ng-model="ctrl.target.currentFilterType" class="gf-form-input" ng-options="filType for filType in ctrl.filterTypes">
+					</select>
+				</div>
+			</div>
+
+			<div class="gf-form">
+				<input type="text" class="gf-form-input" spellcheck='false' bs-typeahead="ctrl.suggestTagValues" data-min-length=0 data-items=100 ng-model="ctrl.target.currentFilterValue" placeholder="filter">
+				</input>
+			</div>
+
+			<gf-form-switch class="gf-form" label="Group by" checked="ctrl.target.currentFilterGroupBy" on-change="ctrl.targetBlur()">
+			</gf-form-switch>
+
+			<div class="gf-form" ng-show="ctrl.addFilterMode">
+				<label class="gf-form-label" ng-show="ctrl.errors.filters">
+					<a bs-tooltip="ctrl.errors.filters" style="color: rgb(229, 189, 28)" >
+						<i class="fa fa-warning"></i>
+					</a>
+				</label>
+
+				<label class="gf-form-label">
+					<a ng-click="ctrl.addFilter()" ng-hide="ctrl.errors.filters">add filter</a>
+					<a ng-click="ctrl.closeAddFilterMode()">
+						<i class="fa fa-remove"></i>
+					</a>
+				</label>
+			</div>
+
+		</div>
+
+		<div class="gf-form gf-form--grow">
+			<div class="gf-form-label gf-form-label--grow"></div>
+		</div>
+	</div>
+
+	<div class="gf-form-inline">
+		<div class="gf-form">
+			<label class="gf-form-label query-keyword width-8">
+				Tags
+				<info-popover mode="right-normal" ng-if="ctrl.tsdbVersion == 2">
+					Please use filters, tags are deprecated in opentsdb 2.2
+				</info-popover>
+			</label>
+		</div>
+
+		<div class="gf-form" ng-repeat="(key, value) in ctrl.target.tags track by $index" class="gf-form">
+			<label class="gf-form-label">
+				{{key}}&nbsp;=&nbsp;{{value}}
+				<a ng-click="ctrl.editTag(key, value)">
+					<i class="fa fa-pencil"></i>
+				</a>
+				<a ng-click="ctrl.removeTag(key)">
+					<i class="fa fa-remove"></i>
+				</a>
+			</label>
+		</div>
+
+		<div class="gf-form" ng-hide="ctrl.addTagMode">
+			<label class="gf-form-label query-keyword">
+				<a ng-click="ctrl.addTag()"><i class="fa fa-plus"></i></a>
+			</label>
+		</div>
+
+		<div class="gf-form" ng-show="ctrl.addTagMode">
+			<input type="text"
+						 class="gf-form-input" spellcheck='false'
+						 bs-typeahead="ctrl.suggestTagKeys" data-min-length=0 data-items=100
+					   ng-model="ctrl.target.currentTagKey" placeholder="key">
+			</input>
+
+			<input type="text" class="gf-form-input"
+						 spellcheck='false' bs-typeahead="ctrl.suggestTagValues"
+						 data-min-length=0 data-items=100 ng-model="ctrl.target.currentTagValue" placeholder="value">
+			</input>
 
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item tight-form-align query-keyword" style="width: 100px">
-			Down sample
-		</li>
-
-		<li>
-			<input type="text" class="input-large tight-form-input"
-			ng-model="ctrl.target.downsampleInterval"
-			ng-model-onblur
-			ng-change="ctrl.targetBlur()"
-			placeholder="interval (empty = auto)"></input>
-		</li>
-
-		<li class="tight-form-item query-keyword">
-			Aggregator
-		</li>
-
-		<li>
-			<select ng-model="ctrl.target.downsampleAggregator" class="tight-form-input input-small"
-				ng-options="agg for agg in ctrl.aggregators"
-				ng-change="ctrl.targetBlur()">
-			</select>
-		</li>
-
-		<li class="tight-form-item query-keyword" style="width: 59px" ng-if="ctrl.tsdbVersion == 2">
-			Fill
-		</li>
-
-		<li ng-if="ctrl.tsdbVersion == 2">
-			<select ng-model="ctrl.target.downsampleFillPolicy" class="tight-form-input input-small"
-				ng-options="agg for agg in ctrl.fillPolicies"
-				ng-change="ctrl.targetBlur()">
-			</select>
-		</li>
-
-		<li class="tight-form-item query-keyword">
-			Disable downsampling <editor-checkbox text="" model="ctrl.target.disableDownsampling" change="ctrl.targetBlur()"></editor-checkbox>
-		</li>
-
-	</ul>
-	<div class="clearfix"></div>
-</div>
-
-<div class="tight-form" ng-if="ctrl.tsdbVersion == 2">
-  <ul class="tight-form-list" role="menu">
-    <li class="tight-form-item tight-form-align query-keyword" style="width: 100px">
-      Filters
-      <tip ng-if="ctrl.tsdbVersion == 2">Filters does not work with tags, either of the two will work but not both.</tip>
-    </li>
-    <li ng-repeat="fil in ctrl.target.filters track by $index" class="tight-form-item">
-      {{fil.tagk}}&nbsp;=&nbsp;{{fil.type}}&#40;{{fil.filter}}&#41;&nbsp;&#44&nbsp;groupBy&nbsp;=&nbsp;{{fil.groupBy}}
-      <a ng-click="ctrl.editFilter(fil, $index)">
-        <i class="fa fa-pencil"></i>
-      </a>
-      <a ng-click="ctrl.removeFilter($index)">
-        <i class="fa fa-remove"></i>
-      </a>
-    </li>
-    <li class="tight-form-item query-keyword" ng-hide="ctrl.addFilterMode">
-      <a ng-click="ctrl.addFilter()">
-        <i class="fa fa-plus"></i>
-      </a>
-    </li>
-
-    <li class="query-keyword" ng-show="ctrl.addFilterMode">
-      <input type="text" class="input-small tight-form-input" spellcheck='false'
-      bs-typeahead="ctrl.suggestTagKeys" data-min-length=0 data-items=100
-      ng-model="ctrl.target.currentFilterKey" placeholder="key"></input>
-
-      Type <select ng-model="ctrl.target.currentFilterType"
-      class="tight-form-input input-small"
-      ng-options="filType for filType in ctrl.filterTypes">
-      </select>
- 
-      <input type="text" class="input-small tight-form-input"
-      spellcheck='false' bs-typeahead="ctrl.suggestTagValues"
-      data-min-length=0 data-items=100 ng-model="ctrl.target.currentFilterValue" placeholder="filter">
-      </input>
-
-      groupBy <editor-checkbox text="" model="ctrl.target.currentFilterGroupBy"></editor-checkbox>
-
-      <a bs-tooltip="ctrl.errors.filters"
-        style="color: rgb(229, 189, 28)"
-        ng-show="ctrl.errors.filters">
-        <i class="fa fa-warning"></i>
-      </a>
- 
-      <a ng-click="ctrl.addFilter()" ng-hide="ctrl.errors.filters">
-        add filter
-      </a>
-      <a ng-click="ctrl.closeAddFilterMode()">
-        <i class="fa fa-remove"></i>
-      </a>
-
-    </li>
-  </ul>
-  <div class="clearfix"></div>
-</div>
-
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item tight-form-align query-keyword" style="width: 100px">
-			Tags
-      <tip ng-if="ctrl.tsdbVersion == 2">Please use filters, tags are deprecated in opentsdb 2.2</tip>
-		</li>
-		<li ng-repeat="(key, value) in ctrl.target.tags track by $index" class="tight-form-item">
-			{{key}}&nbsp;=&nbsp;{{value}}
-			<a ng-click="ctrl.editTag(key, value)">
-				<i class="fa fa-pencil"></i>
-			</a>
-			<a ng-click="ctrl.removeTag(key)">
-				<i class="fa fa-remove"></i>
-			</a>
-		</li>
-
-		<li class="tight-form-item query-keyword" ng-hide="ctrl.addTagMode">
-			<a ng-click="ctrl.addTag()">
-				<i class="fa fa-plus"></i>
-			</a>
-		</li>
-
-		<li ng-show="ctrl.addTagMode">
-			<input type="text" class="input-small tight-form-input" spellcheck='false'
-			bs-typeahead="ctrl.suggestTagKeys" data-min-length=0 data-items=100
-			ng-model="ctrl.target.currentTagKey" placeholder="key"></input>
-
-			<input type="text" class="input-small tight-form-input"
-			spellcheck='false' bs-typeahead="ctrl.suggestTagValues"
-			data-min-length=0 data-items=100 ng-model="ctrl.target.currentTagValue" placeholder="value">
+			<label class="gf-form-label" ng-show="ctrl.errors.tags">
+				<a bs-tooltip="ctrl.errors.tags" style="color: rgb(229, 189, 28)" >
+					<i class="fa fa-warning"></i>
+				</a>
+			</label>
+			<label class="gf-form-label" >
+				<a ng-click="ctrl.addTag()" ng-hide="ctrl.errors.tags">add tag</a>
+				<a ng-click="ctrl.closeAddTagMode()"><i class="fa fa-remove"></i></a>
+			</label>
+		</div>
+
+		<div class="gf-form gf-form--grow">
+			<div class="gf-form-label gf-form-label--grow"></div>
+		</div>
+	</div>
+
+	<div class="gf-form-inline">
+		<gf-form-switch class="gf-form" label="Rate" label-class="width-8 query-keyword" checked="ctrl.target.shouldComputeRate" on-change="ctrl.targetBlur()">
+		</gf-form-switch>
+
+		<gf-form-switch ng-hide="!ctrl.target.shouldComputeRate"
+										class="gf-form" label="Counter" checked="ctrl.target.isCounter" on-change="ctrl.targetBlur()">
+		</gf-form-switch>
+
+
+		<div class="gf-form" ng-hide="!ctrl.target.isCounter || !ctrl.target.shouldComputeRate">
+			<label class="gf-form-label">Counter Max</label>
+			<input type="text" class="gf-form-input"
+					 	 ng-disabled="!ctrl.target.shouldComputeRate"
+						 ng-model="ctrl.target.counterMax" spellcheck='false'
+						 placeholder="max value" ng-model-onblur
+						 ng-blur="ctrl.targetBlur()">
 			</input>
 
-      <a bs-tooltip="ctrl.errors.tags"
-        style="color: rgb(229, 189, 28)"
-        ng-show="ctrl.errors.tags">
-        <i class="fa fa-warning"></i>
-      </a>
-
-			<a ng-click="ctrl.addTag()" ng-hide="ctrl.errors.tags">
-				add tag
-			</a>
-      <a ng-click="ctrl.closeAddTagMode()">
-        <i class="fa fa-remove"></i>
-      </a>
-		
-    </li>
-	</ul>
-	<div class="clearfix"></div>
-</div>
-
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item tight-form-align query-keyword" style="width: 100px">
-			Rate <editor-checkbox text="" model="ctrl.target.shouldComputeRate" change="ctrl.targetBlur()"></editor-checkbox>
-		</li>
-
-		<li class="tight-form-item query-keyword" ng-hide="!ctrl.target.shouldComputeRate">
-			Counter <editor-checkbox text="" model="ctrl.target.isCounter" change="ctrl.targetBlur()"></editor-checkbox>
-		</li>
-
-		<li class="tight-form-item query-keyword" ng-hide="!ctrl.target.isCounter || !ctrl.target.shouldComputeRate">
-			Counter Max:
-		</li>
-
-		<li ng-hide="!ctrl.target.isCounter || !ctrl.target.shouldComputeRate">
-			<input type="text" class="tight-form-input input-small" ng-disabled="!ctrl.target.shouldComputeRate"
-			ng-model="ctrl.target.counterMax" spellcheck='false'
-			placeholder="max value" ng-model-onblur
-			ng-blur="ctrl.targetBlur()"></input>
-		</li>
-		<li class="tight-form-item query-keyword" ng-hide="!ctrl.target.isCounter || !ctrl.target.shouldComputeRate">
-			Reset Value:
-		</li>
-		<li ng-hide="!ctrl.target.isCounter || !ctrl.target.shouldComputeRate">
-			<input type="text" class="tight-form-input input-small" ng-disabled="!ctrl.target.shouldComputeRate"
-			ng-model="ctrl.target.counterResetValue" spellcheck='false'
-			placeholder="reset value" ng-model-onblur
-			ng-blur="ctrl.targetBlur()"></input>
-		</li>
-	</ul>
-
-	<div class="clearfix"></div>
-</div>
+			<label class="gf-form-label">Reset Value</label>
+			<input type="text" class="tight-form-input input-small"
+					   ng-disabled="!ctrl.target.shouldComputeRate"
+						 ng-model="ctrl.target.counterResetValue" spellcheck='false'
+						 placeholder="reset value" ng-model-onblur
+						 ng-blur="ctrl.targetBlur()">
+			</input>
+		</div>
+
+		<div class="gf-form gf-form--grow">
+			<div class="gf-form-label gf-form-label--grow"></div>
+		</div>
+	</div>
+</query-editor-row>
+

+ 48 - 74
public/app/plugins/datasource/prometheus/partials/query.editor.html

@@ -1,80 +1,54 @@
-<query-editor-row ctrl="ctrl">
-	<li class="tight-form-item" style="width: 94px">
-		Query
-	</li>
-	<li>
-		<input type="text"
-		class="input-xxlarge tight-form-input"
-		ng-model="ctrl.target.expr"
-		spellcheck='false'
-		placeholder="query expression"
-		data-min-length=0 data-items=100
-		ng-model-onblur
-		ng-change="ctrl.refreshMetricData()">
-	</li>
-	<li class="tight-form-item">
-		Metric
-	</li>
-	<li>
-		<input type="text"
-		class="input-medium tight-form-input"
-		ng-model="ctrl.target.metric"
-		spellcheck='false'
-		bs-typeahead="ctrl.suggestMetrics"
-		placeholder="metric name"
-		data-min-length=0 data-items=100>
-	</li>
-</query-editor-row>
+<query-editor-row query-ctrl="ctrl" can-collapse="false">
+	<div class="gf-form-inline">
+		<div class="gf-form gf-form--grow">
+			<label class="gf-form-label width-8">Query</label>
+			<input type="text" class="gf-form-input" ng-model="ctrl.target.expr" spellcheck='false' placeholder="query expression" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.refreshMetricData()">
+		</div>
+		<div class="gf-form max-width-22">
+			<label class="gf-form-label">Metric lookup</label>
+			<input type="text" class="gf-form-input" ng-model="ctrl.target.metric" spellcheck='false' bs-typeahead="ctrl.suggestMetrics" placeholder="metric name" data-min-length=0 data-items=100>
+		</div>
+	</div>
 
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item tight-form-align" style="width: 94px">
-			Legend format
-		</li>
-		<li>
-			<input type="text" class="tight-form-input input-xxlarge" ng-model="ctrl.target.legendFormat"
+	<div class="gf-form-inline">
+		<div class="gf-form max-width-26">
+			<label class="gf-form-label width-8">Legend format</label>
+			<input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat"
 			spellcheck='false' placeholder="legend format" data-min-length=0 data-items=1000
 			ng-model-onblur ng-change="ctrl.refreshMetricData()">
 			</input>
-		</li>
-	</ul>
-
-	<div class="clearfix"></div>
-</div>
+		</div>
+		<div class="gf-form">
+			<label class="gf-form-label width-5">Step</label>
+			<input type="text" class="gf-form-input max-width-5" ng-model="ctrl.target.interval"
+					   data-placement="right"
+			       spellcheck='false'
+			       placeholder="{{ctrl.panelCtrl.interval}}"
+			       data-min-length=0 data-items=100
+			       ng-model-onblur
+			       ng-change="ctrl.refreshMetricData()"/>
+			<info-popover mode="right-absolute">
+				Leave blank for auto handling based on time range and panel width
+			</info-popover>
+		</div>
+		<div class="gf-form">
+			<label class="gf-form-label">Resolution</label>
+			<div class="gf-form-select-wrapper max-width-15">
+				<select ng-model="ctrl.target.intervalFactor" class="gf-form-input"
+					ng-options="r.factor as r.label for r in ctrl.resolutions"
+					ng-change="ctrl.refreshMetricData()">
+				</select>
+			</div>
+			<label class="gf-form-label">
+				<a href="{{ctrl.linkToPrometheus}}" target="_blank" bs-tooltip="'Link to Graph in Prometheus'">
+					<i class="fa fa-share-square-o"></i>
+				</a>
+			</label>
+		</div>
 
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item tight-form-align" style="width: 94px">
-			Step
-		</li>
-		<li>
-			<input type="text" class="input-mini tight-form-input" ng-model="ctrl.target.interval"
-			bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
-			data-placement="right"
-			spellcheck='false'
-			placeholder="{{ctrl.panelCtrl.interval}}"
-			data-min-length=0 data-items=100
-			ng-model-onblur
-			ng-change="ctrl.refreshMetricData()"
-			/>
-			</input>
-		</li>
+		<div class="gf-form gf-form--grow">
+			<div class="gf-form-label gf-form-label--grow"></div>
+		</div>
+	</div>
 
-		<li class="tight-form-item">
-			Resolution
-		</li>
-		<li>
-			<select ng-model="ctrl.target.intervalFactor" class="tight-form-input input-mini"
-				ng-options="r.factor as r.label for r in ctrl.resolutions"
-				ng-change="ctrl.refreshMetricData()">
-			</select>
-		</li>
-		<li class="tight-form-item">
-			<a href="{{ctrl.linkToPrometheus}}" target="_blank" bs-tooltip="'Link to Graph in Prometheus'">
-				<i class="fa fa-share-square-o"></i>
-			</a>
-		</li>
-	</ul>
-
-	<div class="clearfix"></div>
-</div>
+</query-editor-row>

+ 11 - 12
public/app/plugins/panel/dashlist/module.ts

@@ -5,27 +5,26 @@ import config from 'app/core/config';
 import {PanelCtrl} from 'app/plugins/sdk';
 import {impressions} from 'app/features/dashboard/impression_store';
 
- // Set and populate defaults
-var panelDefaults = {
-  query: '',
-  limit: 10,
-  tags: [],
-  recent: false,
-  search: false,
-  starred: true,
-  headings: true,
-};
-
 class DashListCtrl extends PanelCtrl {
   static templateUrl = 'module.html';
 
   groups: any[];
   modes: any[];
 
+  panelDefaults = {
+    query: '',
+    limit: 10,
+    tags: [],
+    recent: false,
+    search: false,
+    starred: true,
+    headings: true,
+  };
+
   /** @ngInject */
   constructor($scope, $injector, private backendSrv) {
     super($scope, $injector);
-    _.defaults(this.panel, panelDefaults);
+    _.defaults(this.panel, this.panelDefaults);
 
     if (this.panel.tag) {
       this.panel.tags = [this.panel.tag];

+ 1 - 1
public/app/plugins/panel/graph/graph.js

@@ -73,7 +73,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
             var legendSeries = _.filter(data, function(series) {
               return series.hideFromLegend(panel.legend) === false;
             });
-            var total = 23 + (22 * legendSeries.length);
+            var total = 23 + (21 * legendSeries.length);
             return Math.min(total, Math.floor(panelHeight/2));
           } else {
             return 26;

+ 83 - 83
public/app/plugins/panel/graph/module.ts

@@ -13,85 +13,6 @@ import TimeSeries from 'app/core/time_series2';
 import * as fileExport from 'app/core/utils/file_export';
 import {MetricsPanelCtrl} from 'app/plugins/sdk';
 
-var panelDefaults = {
-  // datasource name, null = default datasource
-  datasource: null,
-  // sets client side (flot) or native graphite png renderer (png)
-  renderer: 'flot',
-  yaxes: [
-    {
-      label: null,
-      show: true,
-      logBase: 1,
-      min: null,
-      max: null,
-      format: 'short'
-    },
-    {
-      label: null,
-      show: true,
-      logBase: 1,
-      min: null,
-      max: null,
-      format: 'short'
-    }
-  ],
-  xaxis: {
-    show: true
-  },
-  grid          : {
-    threshold1: null,
-    threshold2: null,
-    threshold1Color: 'rgba(216, 200, 27, 0.27)',
-    threshold2Color: 'rgba(234, 112, 112, 0.22)'
-  },
-  // show/hide lines
-  lines         : true,
-  // fill factor
-  fill          : 1,
-  // line width in pixels
-  linewidth     : 2,
-  // show hide points
-  points        : false,
-  // point radius in pixels
-  pointradius   : 5,
-  // show hide bars
-  bars          : false,
-  // enable/disable stacking
-  stack         : false,
-  // stack percentage mode
-  percentage    : false,
-  // legend options
-  legend: {
-    show: true, // disable/enable legend
-    values: false, // disable/enable legend values
-    min: false,
-    max: false,
-    current: false,
-    total: false,
-    avg: false
-  },
-  // how null points should be handled
-  nullPointMode : 'connected',
-  // staircase line mode
-  steppedLine: false,
-  // tooltip options
-  tooltip       : {
-    value_type: 'cumulative',
-    shared: true,
-    msResolution: false,
-  },
-  // time overrides
-  timeFrom: null,
-  timeShift: null,
-  // metric queries
-  targets: [{}],
-  // series color overrides
-  aliasColors: {},
-  // other style overrides
-  seriesOverrides: [],
-};
-
 class GraphCtrl extends MetricsPanelCtrl {
   static template = template;
 
@@ -105,14 +26,93 @@ class GraphCtrl extends MetricsPanelCtrl {
   datapointsWarning: boolean;
   colors: any = [];
 
+  panelDefaults = {
+    // datasource name, null = default datasource
+    datasource: null,
+    // sets client side (flot) or native graphite png renderer (png)
+    renderer: 'flot',
+    yaxes: [
+      {
+        label: null,
+        show: true,
+        logBase: 1,
+        min: null,
+        max: null,
+        format: 'short'
+      },
+      {
+        label: null,
+        show: true,
+        logBase: 1,
+        min: null,
+        max: null,
+        format: 'short'
+      }
+    ],
+    xaxis: {
+      show: true
+    },
+    grid          : {
+      threshold1: null,
+      threshold2: null,
+      threshold1Color: 'rgba(216, 200, 27, 0.27)',
+      threshold2Color: 'rgba(234, 112, 112, 0.22)'
+    },
+    // show/hide lines
+    lines         : true,
+    // fill factor
+    fill          : 1,
+    // line width in pixels
+    linewidth     : 2,
+    // show hide points
+    points        : false,
+    // point radius in pixels
+    pointradius   : 5,
+    // show hide bars
+    bars          : false,
+    // enable/disable stacking
+    stack         : false,
+    // stack percentage mode
+    percentage    : false,
+    // legend options
+    legend: {
+      show: true, // disable/enable legend
+      values: false, // disable/enable legend values
+      min: false,
+      max: false,
+      current: false,
+      total: false,
+      avg: false
+    },
+    // how null points should be handled
+    nullPointMode : 'connected',
+    // staircase line mode
+    steppedLine: false,
+    // tooltip options
+    tooltip       : {
+      value_type: 'cumulative',
+      shared: true,
+      msResolution: false,
+    },
+    // time overrides
+    timeFrom: null,
+    timeShift: null,
+    // metric queries
+    targets: [{}],
+    // series color overrides
+    aliasColors: {},
+    // other style overrides
+    seriesOverrides: [],
+  };
+
   /** @ngInject */
   constructor($scope, $injector, private annotationsSrv) {
     super($scope, $injector);
 
-    _.defaults(this.panel, angular.copy(panelDefaults));
-    _.defaults(this.panel.tooltip, panelDefaults.tooltip);
-    _.defaults(this.panel.grid, panelDefaults.grid);
-    _.defaults(this.panel.legend, panelDefaults.legend);
+    _.defaults(this.panel, this.panelDefaults);
+    _.defaults(this.panel.tooltip, this.panelDefaults.tooltip);
+    _.defaults(this.panel.grid, this.panelDefaults.grid);
+    _.defaults(this.panel.legend, this.panelDefaults.legend);
 
     this.colors = $scope.$root.colors;
 

+ 1 - 1
public/app/plugins/panel/pluginlist/module.html

@@ -22,7 +22,7 @@
       </a>
     </div>
     <div class="pluginlist-item" ng-show="category.list.length === 0">
-      <a class="pluginlist-link pluginlist-link-{{plugin.state}}" href="http://grafana.net/plugins/">
+      <a class="pluginlist-link pluginlist-link-{{plugin.state}}" href="https://grafana.net/plugins">
         <span class="pluginlist-none-installed">No additional panels installed. <span class="pluginlist-emphasis">Browse Grafana.net</span></span>
       </a>
     </div>

+ 5 - 5
public/app/plugins/panel/pluginlist/module.ts

@@ -4,20 +4,20 @@ import _ from 'lodash';
 import config from 'app/core/config';
 import {PanelCtrl} from '../../../features/panel/panel_ctrl';
 
-// Set and populate defaults
-var panelDefaults = {
-};
-
 class PluginListCtrl extends PanelCtrl {
   static templateUrl = 'module.html';
 
   pluginList: any[];
   viewModel: any;
 
+  // Set and populate defaults
+  panelDefaults = {
+  };
+
   /** @ngInject */
   constructor($scope, $injector, private backendSrv, private $location) {
     super($scope, $injector);
-    _.defaults(this.panel, panelDefaults);
+    _.defaults(this.panel, this.panelDefaults);
 
     this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
     this.pluginList = [];

+ 49 - 0
public/app/plugins/panel/singlestat/editor.html

@@ -156,6 +156,55 @@
 	</div>
 </div>
 
+<div class="editor-row">
+	<div class="section" style="margin-bottom: 20px">
+		<div class="tight-form">
+			<ul class="tight-form-list">
+				<li class="tight-form-item" style="width: 80px">
+					<strong>Gauge</strong>
+				</li>
+				<li class="tight-form-item">
+					Show&nbsp;
+					<input class="cr1" id="panel.gauge.show" type="checkbox"
+					ng-model="ctrl.panel.gauge.show" ng-checked="ctrl.panel.gauge.show" ng-change="ctrl.render()">
+					<label for="panel.gauge.show" class="cr1"></label>
+				</li>
+				<li class="tight-form-item">
+					Min
+				</li>
+				<li>
+					<input type="number" class="input-small tight-form-input" ng-model="ctrl.panel.gauge.minValue" ng-blur="ctrl.render()" placeholder="0"></input>
+				</li>
+				<li class="tight-form-item last">
+					Max
+				</li>
+				<li>
+					<input type="number" class="input-small tight-form-input last" ng-model="ctrl.panel.gauge.maxValue" ng-blur="ctrl.render()" placeholder="100"></input>
+					<span class="alert-state-critical" ng-show="ctrl.invalidGaugeRange">
+						&nbsp;
+						<i class="fa fa-warning"></i>
+						Min value is bigger than max.
+					</span>
+				</li>
+			</ul>
+			<div class="clearfix"></div>
+		</div>
+		<div class="tight-form last">
+			<li class="tight-form-item">
+				Threshold labels&nbsp;
+				<input class="cr1" id="panel.gauge.thresholdLabels" type="checkbox" ng-model="ctrl.panel.gauge.thresholdLabels" ng-checked="ctrl.panel.gauge.thresholdLabels" ng-change="ctrl.render()">
+				<label for="panel.gauge.thresholdLabels" class="cr1"></label>
+			</li>
+			<li class="tight-form-item">
+				Threshold markers&nbsp;
+				<input class="cr1" id="panel.gauge.thresholdMarkers" type="checkbox" ng-model="ctrl.panel.gauge.thresholdMarkers" ng-checked="ctrl.panel.gauge.thresholdMarkers" ng-change="ctrl.render()">
+				<label for="panel.gauge.thresholdMarkers" class="cr1"></label>
+			</li>
+			<div class="clearfix"></div>
+		</div>
+	</div>
+</div>
+
 <div class="editor-row">
 	<div class="section" style="margin-bottom: 20px">
 		<div class="tight-form last">

+ 152 - 35
public/app/plugins/panel/singlestat/module.ts

@@ -4,43 +4,13 @@ import angular from 'angular';
 import _ from 'lodash';
 import $ from 'jquery';
 import 'jquery.flot';
+import 'jquery.flot.gauge';
 
 import kbn from 'app/core/utils/kbn';
+import config from 'app/core/config';
 import TimeSeries from 'app/core/time_series2';
 import {MetricsPanelCtrl} from 'app/plugins/sdk';
 
-// Set and populate defaults
-var panelDefaults = {
-  links: [],
-  datasource: null,
-  maxDataPoints: 100,
-  interval: null,
-  targets: [{}],
-  cacheTimeout: null,
-  format: 'none',
-  prefix: '',
-  postfix: '',
-  nullText: null,
-  valueMaps: [
-    { value: 'null', op: '=', text: 'N/A' }
-  ],
-  nullPointMode: 'connected',
-  valueName: 'avg',
-  prefixFontSize: '50%',
-  valueFontSize: '80%',
-  postfixFontSize: '50%',
-  thresholds: '',
-  colorBackground: false,
-  colorValue: false,
-  colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
-  sparkline: {
-    show: false,
-    full: false,
-    lineColor: 'rgb(31, 120, 193)',
-    fillColor: 'rgba(31, 118, 189, 0.18)',
-  }
-};
-
 class SingleStatCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
 
@@ -48,11 +18,51 @@ class SingleStatCtrl extends MetricsPanelCtrl {
   data: any;
   fontSizes: any[];
   unitFormats: any[];
+  invalidGaugeRange: boolean;
+
+  // Set and populate defaults
+  panelDefaults = {
+    links: [],
+    datasource: null,
+    maxDataPoints: 100,
+    interval: null,
+    targets: [{}],
+    cacheTimeout: null,
+    format: 'none',
+    prefix: '',
+    postfix: '',
+    nullText: null,
+    valueMaps: [
+      { value: 'null', op: '=', text: 'N/A' }
+    ],
+    nullPointMode: 'connected',
+    valueName: 'avg',
+    prefixFontSize: '50%',
+    valueFontSize: '80%',
+    postfixFontSize: '50%',
+    thresholds: '',
+    colorBackground: false,
+    colorValue: false,
+    colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+    sparkline: {
+      show: false,
+      full: false,
+      lineColor: 'rgb(31, 120, 193)',
+      fillColor: 'rgba(31, 118, 189, 0.18)',
+    },
+    gauge: {
+      show: false,
+      minValue: 0,
+      maxValue: 100,
+      thresholdMarkers: true,
+      thresholdLabels: false
+    }
+  };
 
   /** @ngInject */
   constructor($scope, $injector, private $location, private linkSrv) {
     super($scope, $injector);
-    _.defaults(this.panel, panelDefaults);
+    _.defaults(this.panel, this.panelDefaults);
 
     this.events.on('data-received', this.onDataReceived.bind(this));
     this.events.on('data-error', this.onDataError.bind(this));
@@ -270,6 +280,109 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       return body;
     }
 
+    function getValueText() {
+      var result = panel.prefix ? panel.prefix : '';
+      result += data.valueFormated;
+      result += panel.postfix ? panel.postfix : '';
+
+      return result;
+    }
+
+    function addGauge() {
+      ctrl.invalidGaugeRange = false;
+      if (panel.gauge.minValue > panel.gauge.maxValue) {
+        ctrl.invalidGaugeRange = true;
+        return;
+      }
+
+      var plotCanvas = $('<div></div>');
+      var width = elem.width();
+      var height = elem.height();
+      var plotCss = {
+        top: '10px',
+        margin: 'auto',
+        position: 'relative',
+        height: (height * 0.9) + 'px',
+        width:  width + 'px'
+      };
+
+      plotCanvas.css(plotCss);
+
+      var thresholds = [];
+      for (var i = 0; i < data.thresholds.length; i++) {
+        thresholds.push({
+          value: data.thresholds[i],
+          color: data.colorMap[i]
+        });
+      }
+      thresholds.push({
+        value: panel.gauge.maxValue,
+        color: data.colorMap[data.colorMap.length  - 1]
+      });
+
+      var bgColor = config.bootData.user.lightTheme
+        ? 'rgb(230,230,230)'
+        : 'rgb(38,38,38)';
+
+
+      var dimension = Math.min(width, height);
+      var fontSize = Math.min(dimension/4, 100);
+      var gaugeWidth = Math.min(dimension/6, 60);
+      var thresholdMarkersWidth = gaugeWidth/5;
+
+      var options = {
+        series: {
+          gauges: {
+            gauge: {
+              min: panel.gauge.minValue,
+              max: panel.gauge.maxValue,
+              background: { color: bgColor },
+              border: { color: null },
+              shadow: { show: false },
+              width: gaugeWidth,
+            },
+            frame: { show: false },
+            label: { show: false },
+            layout: { margin: 0, thresholdWidth: 0 },
+            cell: { border: { width: 0 } },
+            threshold: {
+              values: thresholds,
+              label: {
+                show: panel.gauge.thresholdLabels,
+                margin: 8,
+                font: { size: 18 }
+              },
+              show: panel.gauge.thresholdMarkers,
+              width: thresholdMarkersWidth,
+            },
+            value: {
+              color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null,
+              formatter: function() { return getValueText(); },
+              font: { size: fontSize, family: 'Helvetica Neue", Helvetica, Arial, sans-serif' }
+            },
+            show: true
+          }
+        }
+      };
+
+      elem.append(plotCanvas);
+
+      var plotSeries = {
+        data: [[0, data.valueRounded]]
+      };
+
+      $.plot(plotCanvas, [plotSeries], options);
+    }
+
+    function getGaugeFontSize() {
+      if (panel.valueFontSize) {
+        var num = parseInt(panel.valueFontSize.substring(0, panel.valueFontSize.length - 1));
+        return (30 * (num / 100)) + 15;
+      } else {
+        return 30;
+      }
+    }
+
     function addSparkline() {
       var width = elem.width() + 20;
       if (width < 30) {
@@ -331,11 +444,11 @@ class SingleStatCtrl extends MetricsPanelCtrl {
 
     function render() {
       if (!ctrl.data) { return; }
-
+      ctrl.setValues(ctrl.data);
       data = ctrl.data;
       setElementHeight();
 
-      var body = getBigValueHtml();
+      var body = panel.gauge.show ? '' : getBigValueHtml();
 
       if (panel.colorBackground && !isNaN(data.valueRounded)) {
         var color = getColorForValue(data, data.valueRounded);
@@ -358,6 +471,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
         addSparkline();
       }
 
+      if (panel.gauge.show) {
+        addGauge();
+      }
+
       elem.toggleClass('pointer', panel.links.length > 0);
 
       if (panel.links.length > 0) {

+ 33 - 28
public/app/plugins/panel/table/module.ts

@@ -10,33 +10,6 @@ import {transformDataToTable} from './transformers';
 import {tablePanelEditor} from './editor';
 import {TableRenderer} from './renderer';
 
-var panelDefaults = {
-  targets: [{}],
-  transform: 'timeseries_to_columns',
-  pageSize: null,
-  showHeader: true,
-  styles: [
-    {
-      type: 'date',
-      pattern: 'Time',
-      dateFormat: 'YYYY-MM-DD HH:mm:ss',
-    },
-    {
-      unit: 'short',
-      type: 'number',
-      decimals: 2,
-      colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
-      colorMode: null,
-      pattern: '/.*/',
-      thresholds: [],
-    }
-  ],
-  columns: [],
-  scroll: true,
-  fontSize: '100%',
-  sort: {col: 0, desc: true},
-};
-
 class TablePanelCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
 
@@ -44,6 +17,33 @@ class TablePanelCtrl extends MetricsPanelCtrl {
   dataRaw: any;
   table: any;
 
+  panelDefaults = {
+    targets: [{}],
+    transform: 'timeseries_to_columns',
+    pageSize: null,
+    showHeader: true,
+    styles: [
+      {
+        type: 'date',
+        pattern: 'Time',
+        dateFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+      {
+        unit: 'short',
+        type: 'number',
+        decimals: 2,
+        colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+        colorMode: null,
+        pattern: '/.*/',
+        thresholds: [],
+      }
+    ],
+    columns: [],
+    scroll: true,
+    fontSize: '100%',
+    sort: {col: 0, desc: true},
+  };
+
   /** @ngInject */
   constructor($scope, $injector, private annotationsSrv) {
     super($scope, $injector);
@@ -56,7 +56,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
       delete this.panel.fields;
     }
 
-    _.defaults(this.panel, panelDefaults);
+    _.defaults(this.panel, this.panelDefaults);
 
     this.events.on('data-received', this.onDataReceived.bind(this));
     this.events.on('data-error', this.onDataError.bind(this));
@@ -120,6 +120,11 @@ class TablePanelCtrl extends MetricsPanelCtrl {
   }
 
   toggleColumnSort(col, colIndex) {
+    // remove sort flag from current column
+    if (this.table.columns[this.panel.sort.col]) {
+      this.table.columns[this.panel.sort.col].sort = false;
+    }
+
     if (this.panel.sort.col === colIndex) {
       if (this.panel.sort.desc) {
         this.panel.sort.desc = false;

+ 1 - 1
public/app/plugins/panel/table/renderer.ts

@@ -30,7 +30,7 @@ export class TableRenderer {
     }
 
     if (_.isArray(v)) {
-      v = v.join(',&nbsp;');
+      v = v.join(', ');
     }
 
     return v;

+ 6 - 8
public/app/plugins/panel/text/module.ts

@@ -3,23 +3,21 @@
 import _ from 'lodash';
 import {PanelCtrl} from 'app/plugins/sdk';
 
- // Set and populate defaults
-var panelDefaults = {
-  mode    : "markdown", // 'html', 'markdown', 'text'
-  content : "# title",
-};
-
 export class TextPanelCtrl extends PanelCtrl {
   static templateUrl = `public/app/plugins/panel/text/module.html`;
 
   remarkable: any;
   content: string;
-
+  // Set and populate defaults
+  panelDefaults = {
+    mode    : "markdown", // 'html', 'markdown', 'text'
+    content : "# title",
+  };
   /** @ngInject */
   constructor($scope, $injector, private templateSrv, private $sce) {
     super($scope, $injector);
 
-    _.defaults(this.panel, panelDefaults);
+    _.defaults(this.panel, this.panelDefaults);
 
     this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
     this.events.on('refresh', this.onRender.bind(this));

+ 2 - 1
public/app/system.conf.js

@@ -27,7 +27,8 @@ System.config({
     "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent",
     "jquery.flot.time": "vendor/flot/jquery.flot.time",
     "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair",
-    "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow"
+    "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
+    "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge"
   },
 
   packages: {

+ 1 - 0
public/sass/_grafana.scss

@@ -70,6 +70,7 @@
 @import "components/drop";
 @import "components/query_editor";
 @import "components/tabbed_view";
+@import "components/query_part";
 
 // PAGES
 @import "pages/login";

+ 3 - 3
public/sass/_variables.dark.scss

@@ -67,8 +67,8 @@ $page-gradient: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98%
 
 // Links
 // -------------------------
-$link-color:              darken($white,11%);
-$link-color-disabled:     darken($link-color,30%);
+$link-color:              darken($white, 11%);
+$link-color-disabled:     darken($link-color, 30%);
 $link-hover-color:        $white;
 $external-link-color:     $blue;
 
@@ -76,7 +76,7 @@ $external-link-color:     $blue;
 // -------------------------
 $headings-color:       darken($white,11%);
 $abbr-border-color: 	 $gray-3 !default;
-$text-muted: 			     darken($link-color,30%);
+$text-muted: 			     $text-color-weak;
 
 $blockquote-small-color:  $gray-3 !default;
 $blockquote-border-color: $gray-4 !default;

+ 1 - 1
public/sass/_variables.light.scss

@@ -82,7 +82,7 @@ $external-link-color:    $blue;
 // -------------------------
 $headings-color:       $text-color;
 $abbr-border-color: 	 $gray-2 !default;
-$text-muted: 			     darken($link-color,30%);
+$text-muted: 			     $text-color-weak;
 
 $blockquote-small-color:  $gray-2 !default;
 $blockquote-border-color: $gray-3 !default;

Неке датотеке нису приказане због велике количине промена