瀏覽代碼

Merge branch 'master' into export-dashboard

Conflicts:
	public/app/features/dashboard/submenu/submenu.ts
Torkel Ödegaard 9 年之前
父節點
當前提交
ba6573af61
共有 48 個文件被更改,包括 423 次插入185 次删除
  1. 6 7
      .github/ISSUE_TEMPLATE.md
  2. 22 2
      CHANGELOG.md
  3. 1 1
      docs/sources/installation/configuration.md
  4. 3 3
      docs/sources/installation/debian.md
  5. 4 4
      docs/sources/installation/rpm.md
  6. 1 1
      docs/sources/installation/windows.md
  7. 2 2
      latest.json
  8. 10 12
      packaging/publish/publish.sh
  9. 1 1
      pkg/api/api.go
  10. 6 5
      pkg/api/cloudwatch/cloudwatch.go
  11. 11 14
      pkg/api/dashboard.go
  12. 11 7
      pkg/api/dtos/index.go
  13. 9 5
      pkg/api/index.go
  14. 1 1
      pkg/api/pluginproxy/pluginproxy.go
  15. 1 1
      pkg/cmd/grafana-server/main.go
  16. 0 9
      public/app/core/controllers/login_ctrl.js
  17. 3 1
      public/app/core/directives/metric_segment.js
  18. 29 22
      public/app/core/services/datasource_srv.js
  19. 2 2
      public/app/core/time_series2.ts
  20. 1 1
      public/app/features/annotations/editor_ctrl.js
  21. 16 3
      public/app/features/dashboard/rowCtrl.js
  22. 2 2
      public/app/features/dashboard/shareModalCtrl.js
  23. 24 1
      public/app/features/dashboard/submenu/submenu.ts
  24. 8 1
      public/app/features/dashboard/timeSrv.js
  25. 19 1
      public/app/features/panel/panel_ctrl.ts
  26. 1 1
      public/app/features/panel/panel_directive.ts
  27. 16 9
      public/app/features/templating/templateSrv.js
  28. 25 4
      public/app/features/templating/templateValuesSrv.js
  29. 0 9
      public/app/partials/login.html
  30. 0 1
      public/app/partials/signup_step2.html
  31. 11 3
      public/app/plugins/datasource/elasticsearch/datasource.js
  32. 3 3
      public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html
  33. 8 17
      public/app/plugins/datasource/prometheus/datasource.ts
  34. 4 0
      public/app/plugins/datasource/prometheus/query_ctrl.ts
  35. 1 1
      public/app/plugins/panel/graph/graph.js
  36. 9 9
      public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
  37. 0 1
      public/app/plugins/panel/singlestat/module.html
  38. 3 2
      public/app/plugins/panel/singlestat/module.ts
  39. 4 0
      public/sass/_variables.dark.scss
  40. 4 0
      public/sass/_variables.light.scss
  41. 36 7
      public/sass/components/_footer.scss
  42. 0 1
      public/sass/components/_panel_singlestat.scss
  43. 1 1
      public/sass/layout/_page.scss
  44. 3 3
      public/test/core/time_series_specs.js
  45. 11 2
      public/test/specs/templateSrv-specs.js
  46. 53 0
      public/test/specs/templateValuesSrv-specs.js
  47. 35 0
      public/views/index.html
  48. 2 2
      vendor/phantomjs/render.js

+ 6 - 7
.github/ISSUE_TEMPLATE.md

@@ -1,20 +1,19 @@
-Thank you! For helping us make Grafana even better.
+Thank you for helping us make Grafana even better!
 
 
-To help us respond to your issues faster, please make sure to add as much information as possible.
+To help us respond to your issues more quickly, please make sure to add as much information as possible.
 
 
-If this issue is about a plugin, please open the issue in that repository.
+If this issue is about a plugin, please open the issue in that plugin's repository.
 
 
-Start your issues title with [Feature Request] / [Bug] / [Question] or no tag if your unsure. Also, please be aware that GitHub now supports uploading of screenshots; look at the bottom of this input field.
+Start your issue's title with [Feature Request] / [Bug] / [Question] or no tag if you're unsure. Also, please be aware that GitHub now supports uploading of screenshots; look at the bottom of this input field.
 
 
 Please include some basic information:
 Please include some basic information:
-- What grafana version are you using?
+- What Grafana version are you using?
 - What datasource are you using?
 - What datasource are you using?
 - What OS are you running grafana on?
 - What OS are you running grafana on?
 - What did you do?
 - What did you do?
 - What was the expected result?
 - What was the expected result?
 - What happenend instead?
 - What happenend instead?
 
 
-If you question/bug relates to a metric query / unexpected data visualization, please include:
+If your question/bug relates to a metric query / unexpected data visualization, please include:
 - An image or text representation of your metric query
 - An image or text representation of your metric query
 - The raw query and response from your data source (check this in chrome dev tools network tab)
 - The raw query and response from your data source (check this in chrome dev tools network tab)
-

+ 22 - 2
CHANGELOG.md

@@ -1,13 +1,33 @@
-# 3.1.0
+# 3.1.0 (unreleased)
 
 
 ### Enhancements
 ### Enhancements
+* **Dashboard Url**: Time range changes updates url, closes [#458](https://github.com/grafana/grafana/issues/458)
+* **Dashboard Url**: Template variable change updates url, closes [#5002](https://github.com/grafana/grafana/issues/5002)
 * **Singlestat**: Add support for range to text mappings, closes [#1319](https://github.com/grafana/grafana/issues/1319)
 * **Singlestat**: Add support for range to text mappings, closes [#1319](https://github.com/grafana/grafana/issues/1319)
 * **Graph**: Adds sort order options for graph tooltip, closes  [#1189](https://github.com/grafana/grafana/issues/1189)
 * **Graph**: Adds sort order options for graph tooltip, closes  [#1189](https://github.com/grafana/grafana/issues/1189)
 * **Theme**: Add default theme to config file [#5011](https://github.com/grafana/grafana/pull/5011)
 * **Theme**: Add default theme to config file [#5011](https://github.com/grafana/grafana/pull/5011)
+* **Page Footer**: Added page footer with links to docs, shows Grafana version and info if new version is available, closes [#4889](https://github.com/grafana/grafana/pull/4889)
 
 
-# 3.0.3 Patch release (unreleased)
+# 3.0.4 Patch release (2016-05-25)
+* **Panel**: Fixed blank dashboard issue when switching to other dashboard while in fullscreen edit mode, fixes [#5163](https://github.com/grafana/grafana/pull/5163)
+* **Templating**: Fixed issue with nested multi select variables and cascading and updating child variable selection state, fixes [#4861](https://github.com/grafana/grafana/pull/4861)
+* **Templating**: Fixed issue with using templated data source in another template variable query, fixes [#5165](https://github.com/grafana/grafana/pull/5165)
+* **Singlestat gauge**: Fixed issue with gauge render position, fixes [#5143](https://github.com/grafana/grafana/pull/5143)
+* **Home dashboard**: Fixes broken home dashboard api, fixes [#5167](https://github.com/grafana/grafana/issues/5167)
+
+# 3.0.3 Patch release (2016-05-23)
+* **Annotations**: Annotations can now use a template variable as data source, closes [#5054](https://github.com/grafana/grafana/issues/5054)
 * **Time picker**: Fixed issue timepicker and UTC when reading time from URL, fixes [#5078](https://github.com/grafana/grafana/issues/5078)
 * **Time picker**: Fixed issue timepicker and UTC when reading time from URL, fixes [#5078](https://github.com/grafana/grafana/issues/5078)
 * **CloudWatch**: Support for Multiple Account by AssumeRole, closes [#3522](https://github.com/grafana/grafana/issues/3522)
 * **CloudWatch**: Support for Multiple Account by AssumeRole, closes [#3522](https://github.com/grafana/grafana/issues/3522)
+* **Singlestat**: Fixed alignment and minium height issue, fixes [#5113](https://github.com/grafana/grafana/issues/5113), fixes [#4679](https://github.com/grafana/grafana/issues/4679)
+* **Share modal**: Fixed link when using grafana under dashboard sub url, fixes [#5109](https://github.com/grafana/grafana/issues/5109)
+* **Prometheus**: Fixed bug in query editor that caused it not to load when reloading page, fixes [#5107](https://github.com/grafana/grafana/issues/5107)
+* **Elasticsearch**: Fixed bug when template variable query returns numeric values, fixes [#5097](https://github.com/grafana/grafana/issues/5097), fixes [#5088](https://github.com/grafana/grafana/issues/5088)
+* **Logging**: Fixed issue with reading logging level value, fixes [#5079](https://github.com/grafana/grafana/issues/5079)
+* **Timepicker**: Fixed issue with timepicker and UTC when reading time from URL, fixes [#5078](https://github.com/grafana/grafana/issues/5078)
+* **Docs**: Added docs for org & user preferences HTTP API, closes [#5069](https://github.com/grafana/grafana/issues/5069)
+* **Plugin list panel**: Now shows correct enable state for apps when not enabled, fixes [#5068](https://github.com/grafana/grafana/issues/5068)
+* **Elasticsearch**: Templating & Annotation queries that use template variables are now formatted correctly, fixes [#5135](https://github.com/grafana/grafana/issues/5135)
 
 
 # 3.0.2 Patch release (2016-05-16)
 # 3.0.2 Patch release (2016-05-16)
 
 

+ 1 - 1
docs/sources/installation/configuration.md

@@ -226,7 +226,7 @@ organization to be created for that new user.
 
 
 The role new users will be assigned for the main organization (if the
 The role new users will be assigned for the main organization (if the
 above setting is set to true).  Defaults to `Viewer`, other valid
 above setting is set to true).  Defaults to `Viewer`, other valid
-options are `Admin` and `Editor`.
+options are `Admin` and `Editor` and `Read-Only Editor`.
 
 
 <hr>
 <hr>
 
 

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

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

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

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

+ 1 - 1
docs/sources/installation/windows.md

@@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-Stable Zip package for Windows | [grafana.3.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.0.2.windows-x64.zip)
+Stable Zip package for Windows | [grafana.3.0.4.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.0.4.windows-x64.zip)
 
 
 ## Configure
 ## Configure
 
 

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
 {
-  "stable": "3.0.2",
-	"testing": "3.0.2"
+  "stable": "3.0.4",
+	"testing": "3.0.4"
 }
 }

+ 10 - 12
packaging/publish/publish.sh

@@ -1,22 +1,20 @@
 #! /usr/bin/env bash
 #! /usr/bin/env bash
 
 
-deb_ver=3.0.1
-rpm_ver=3.0.1-1
+deb_ver=3.0.4-1464167696
+rpm_ver=3.0.4-1464167696
 
 
-#rpm_ver=3.0.0-1
+wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
 
 
-#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
+package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb
+package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb
 
 
-#package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb
-#package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb
+package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb
+package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb
 
 
-#package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb
-#package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb
+wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm
 
 
-#wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm
-
-#package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm
-#package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm
+package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm
+package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm
 
 
 package_cloud push grafana/stable/el/7 grafana-${rpm_ver}.x86_64.rpm
 package_cloud push grafana/stable/el/7 grafana-${rpm_ver}.x86_64.rpm
 package_cloud push grafana/stable/el/6 grafana-${rpm_ver}.x86_64.rpm
 package_cloud push grafana/stable/el/6 grafana-${rpm_ver}.x86_64.rpm

+ 1 - 1
pkg/api/api.go

@@ -211,7 +211,7 @@ func Register(r *macaron.Macaron) {
 			r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
 			r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
 			r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard)
 			r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard)
 			r.Get("/file/:file", GetDashboardFromJsonFile)
 			r.Get("/file/:file", GetDashboardFromJsonFile)
-			r.Get("/home", GetHomeDashboard)
+			r.Get("/home", wrap(GetHomeDashboard))
 			r.Get("/tags", GetDashboardTags)
 			r.Get("/tags", GetDashboardTags)
 			r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
 			r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
 		})
 		})

+ 6 - 5
pkg/api/cloudwatch/cloudwatch.go

@@ -57,11 +57,12 @@ var awsCredentialCache map[string]cache = make(map[string]cache)
 var credentialCacheLock sync.RWMutex
 var credentialCacheLock sync.RWMutex
 
 
 func getCredentials(profile string, region string, assumeRoleArn string) *credentials.Credentials {
 func getCredentials(profile string, region string, assumeRoleArn string) *credentials.Credentials {
+	cacheKey := profile + ":" + assumeRoleArn
 	credentialCacheLock.RLock()
 	credentialCacheLock.RLock()
-	if _, ok := awsCredentialCache[profile]; ok {
-		if awsCredentialCache[profile].expiration != nil &&
-			(*awsCredentialCache[profile].expiration).After(time.Now().UTC()) {
-			result := awsCredentialCache[profile].credential
+	if _, ok := awsCredentialCache[cacheKey]; ok {
+		if awsCredentialCache[cacheKey].expiration != nil &&
+			(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
+			result := awsCredentialCache[cacheKey].credential
 			credentialCacheLock.RUnlock()
 			credentialCacheLock.RUnlock()
 			return result
 			return result
 		}
 		}
@@ -118,7 +119,7 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden
 			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
 			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
 		})
 		})
 	credentialCacheLock.Lock()
 	credentialCacheLock.Lock()
-	awsCredentialCache[profile] = cache{
+	awsCredentialCache[cacheKey] = cache{
 		credential: creds,
 		credential: creds,
 		expiration: expiration,
 		expiration: expiration,
 	}
 	}

+ 11 - 14
pkg/api/dashboard.go

@@ -8,6 +8,7 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
@@ -158,30 +159,27 @@ func canEditDashboard(role m.RoleType) bool {
 	return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
 	return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
 }
 }
 
 
-func GetHomeDashboard(c *middleware.Context) {
+func GetHomeDashboard(c *middleware.Context) Response {
 	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
 	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
 	if err := bus.Dispatch(&prefsQuery); err != nil {
 	if err := bus.Dispatch(&prefsQuery); err != nil {
-		c.JsonApiErr(500, "Failed to get preferences", err)
+		return ApiError(500, "Failed to get preferences", err)
 	}
 	}
 
 
 	if prefsQuery.Result.HomeDashboardId != 0 {
 	if prefsQuery.Result.HomeDashboardId != 0 {
 		slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
 		slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
 		err := bus.Dispatch(&slugQuery)
 		err := bus.Dispatch(&slugQuery)
-		if err != nil {
-			c.JsonApiErr(500, "Failed to get slug from database", err)
-			return
+		if err == nil {
+			dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result}
+			return Json(200, &dashRedirect)
+		} else {
+			log.Warn("Failed to get slug from database, %s", err.Error())
 		}
 		}
-
-		dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result}
-		c.JSON(200, &dashRedirect)
-		return
 	}
 	}
 
 
 	filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
 	filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
 	file, err := os.Open(filePath)
 	file, err := os.Open(filePath)
 	if err != nil {
 	if err != nil {
-		c.JsonApiErr(500, "Failed to load home dashboard", err)
-		return
+		return ApiError(500, "Failed to load home dashboard", err)
 	}
 	}
 
 
 	dash := dtos.DashboardFullWithMeta{}
 	dash := dtos.DashboardFullWithMeta{}
@@ -189,11 +187,10 @@ func GetHomeDashboard(c *middleware.Context) {
 	dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
 	dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
 	jsonParser := json.NewDecoder(file)
 	jsonParser := json.NewDecoder(file)
 	if err := jsonParser.Decode(&dash.Dashboard); err != nil {
 	if err := jsonParser.Decode(&dash.Dashboard); err != nil {
-		c.JsonApiErr(500, "Failed to load home dashboard", err)
-		return
+		return ApiError(500, "Failed to load home dashboard", err)
 	}
 	}
 
 
-	c.JSON(200, &dash)
+	return Json(200, &dash)
 }
 }
 
 
 func GetDashboardFromJsonFile(c *middleware.Context) {
 func GetDashboardFromJsonFile(c *middleware.Context) {

+ 11 - 7
pkg/api/dtos/index.go

@@ -1,13 +1,17 @@
 package dtos
 package dtos
 
 
 type IndexViewData struct {
 type IndexViewData struct {
-	User               *CurrentUser
-	Settings           map[string]interface{}
-	AppUrl             string
-	AppSubUrl          string
-	GoogleAnalyticsId  string
-	GoogleTagManagerId string
-	MainNavLinks       []*NavLink
+	User                    *CurrentUser
+	Settings                map[string]interface{}
+	AppUrl                  string
+	AppSubUrl               string
+	GoogleAnalyticsId       string
+	GoogleTagManagerId      string
+	MainNavLinks            []*NavLink
+	BuildVersion            string
+	BuildCommit             string
+	NewGrafanaVersionExists bool
+	NewGrafanaVersion       string
 }
 }
 
 
 type PluginCss struct {
 type PluginCss struct {

+ 9 - 5
pkg/api/index.go

@@ -36,11 +36,15 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 			LightTheme:     prefs.Theme == "light",
 			LightTheme:     prefs.Theme == "light",
 			Timezone:       prefs.Timezone,
 			Timezone:       prefs.Timezone,
 		},
 		},
-		Settings:           settings,
-		AppUrl:             setting.AppUrl,
-		AppSubUrl:          setting.AppSubUrl,
-		GoogleAnalyticsId:  setting.GoogleAnalyticsId,
-		GoogleTagManagerId: setting.GoogleTagManagerId,
+		Settings:                settings,
+		AppUrl:                  setting.AppUrl,
+		AppSubUrl:               setting.AppSubUrl,
+		GoogleAnalyticsId:       setting.GoogleAnalyticsId,
+		GoogleTagManagerId:      setting.GoogleTagManagerId,
+		BuildVersion:            setting.BuildVersion,
+		BuildCommit:             setting.BuildCommit,
+		NewGrafanaVersion:       plugins.GrafanaLatestVersion,
+		NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
 	}
 	}
 
 
 	if setting.DisableGravatar {
 	if setting.DisableGravatar {

+ 1 - 1
pkg/api/pluginproxy/pluginproxy.go

@@ -88,7 +88,7 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins
 			}
 			}
 
 
 			for key, value := range headers {
 			for key, value := range headers {
-				log.Info("setting key %v value %v", key, value[0])
+				log.Trace("setting key %v value %v", key, value[0])
 				req.Header.Set(key, value[0])
 				req.Header.Set(key, value[0])
 			}
 			}
 		}
 		}

+ 1 - 1
pkg/cmd/grafana-server/main.go

@@ -24,7 +24,7 @@ import (
 	"github.com/grafana/grafana/pkg/social"
 	"github.com/grafana/grafana/pkg/social"
 )
 )
 
 
-var version = "3.0.0-beta4"
+var version = "3.1.0"
 var commit = "NA"
 var commit = "NA"
 var buildstamp string
 var buildstamp string
 var build_date string
 var build_date string

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

@@ -35,15 +35,6 @@ function (angular, coreModule, config) {
       }
       }
     };
     };
 
 
-    // build info view model
-    $scope.buildInfo = {
-      version: config.buildInfo.version,
-      commit: config.buildInfo.commit,
-      buildstamp: new Date(config.buildInfo.buildstamp * 1000),
-      latestVersion: config.buildInfo.latestVersion,
-      hasUpdate: config.buildInfo.hasUpdate,
-    };
-
     $scope.submit = function() {
     $scope.submit = function() {
       if ($scope.loginMode) {
       if ($scope.loginMode) {
         $scope.login();
         $scope.login();

+ 3 - 1
public/app/core/directives/metric_segment.js

@@ -209,7 +209,9 @@ function (_, $, coreModule) {
             // needs to call this after digest so
             // needs to call this after digest so
             // property is synced with outerscope
             // property is synced with outerscope
             $scope.$$postDigest(function() {
             $scope.$$postDigest(function() {
-              $scope.onChange();
+              $scope.$apply(function() {
+                $scope.onChange();
+              });
             });
             });
           };
           };
 
 

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

@@ -66,14 +66,17 @@ function (angular, _, coreModule, config) {
     };
     };
 
 
     this.getAnnotationSources = function() {
     this.getAnnotationSources = function() {
-      return _.reduce(config.datasources, function(memo, value) {
+      var sources = [];
 
 
+      this.addDataSourceVariables(sources);
+
+      _.each(config.datasources, function(value) {
         if (value.meta && value.meta.annotations) {
         if (value.meta && value.meta.annotations) {
-          memo.push(value);
+          sources.push(value);
         }
         }
+      });
 
 
-        return memo;
-      }, []);
+      return sources;
     };
     };
 
 
     this.getMetricSources = function(options) {
     this.getMetricSources = function(options) {
@@ -90,24 +93,7 @@ function (angular, _, coreModule, config) {
       });
       });
 
 
       if (!options || !options.skipVariables) {
       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,
-            });
-          }
-        }
+        this.addDataSourceVariables(metricSources);
       }
       }
 
 
       metricSources.sort(function(a, b) {
       metricSources.sort(function(a, b) {
@@ -123,6 +109,27 @@ function (angular, _, coreModule, config) {
       return metricSources;
       return metricSources;
     };
     };
 
 
+    this.addDataSourceVariables = function(list) {
+      // 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) {
+          list.push({
+            name: '$' + variable.name,
+            value: '$' + variable.name,
+            meta: ds.meta,
+          });
+        }
+      }
+    };
+
     this.init();
     this.init();
   });
   });
 });
 });

+ 2 - 2
public/app/core/time_series2.ts

@@ -173,8 +173,8 @@ export default class TimeSeries {
 
 
   isMsResolutionNeeded() {
   isMsResolutionNeeded() {
     for (var i = 0; i < this.datapoints.length; i++) {
     for (var i = 0; i < this.datapoints.length; i++) {
-      if (this.datapoints[i][0] !== null) {
-        var timestamp = this.datapoints[i][0].toString();
+      if (this.datapoints[i][1] !== null) {
+        var timestamp = this.datapoints[i][1].toString();
         if (timestamp.length === 13 && (timestamp % 1000) !== 0) {
         if (timestamp.length === 13 && (timestamp % 1000) !== 0) {
           return true;
           return true;
         }
         }

+ 1 - 1
public/app/features/annotations/editor_ctrl.js

@@ -30,7 +30,7 @@ function (angular, _, $) {
     $scope.datasourceChanged = function() {
     $scope.datasourceChanged = function() {
       return datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
       return datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
         $scope.currentDatasource = ds;
         $scope.currentDatasource = ds;
-        $scope.currentAnnotation.datasource = ds.name;
+        $scope.currentAnnotation.datasource = $scope.currentAnnotation.datasource;
       });
       });
     };
     };
 
 

+ 16 - 3
public/app/features/dashboard/rowCtrl.js

@@ -142,12 +142,19 @@ function (angular, _, config) {
   });
   });
 
 
   module.directive('panelWidth', function() {
   module.directive('panelWidth', function() {
+
     return function(scope, element) {
     return function(scope, element) {
+      var fullscreen = false;
+
       function updateWidth() {
       function updateWidth() {
-        element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
+        if (!fullscreen) {
+          element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
+        }
       }
       }
 
 
       scope.onAppEvent('panel-fullscreen-enter', function(evt, info) {
       scope.onAppEvent('panel-fullscreen-enter', function(evt, info) {
+        fullscreen = true;
+
         if (scope.panel.id !== info.panelId) {
         if (scope.panel.id !== info.panelId) {
           element.hide();
           element.hide();
         } else {
         } else {
@@ -156,14 +163,20 @@ function (angular, _, config) {
       });
       });
 
 
       scope.onAppEvent('panel-fullscreen-exit', function(evt, info) {
       scope.onAppEvent('panel-fullscreen-exit', function(evt, info) {
+        fullscreen = false;
+
         if (scope.panel.id !== info.panelId) {
         if (scope.panel.id !== info.panelId) {
           element.show();
           element.show();
-        } else {
-          updateWidth();
         }
         }
+
+        updateWidth();
       });
       });
 
 
       scope.$watch('panel.span', updateWidth);
       scope.$watch('panel.span', updateWidth);
+
+      if (fullscreen) {
+        element.hide();
+      }
     };
     };
   });
   });
 
 

+ 2 - 2
public/app/features/dashboard/shareModalCtrl.js

@@ -74,12 +74,12 @@ function (angular, _, require, config) {
       $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
       $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
 
 
       var soloUrl = $scope.shareUrl;
       var soloUrl = $scope.shareUrl;
-      soloUrl = soloUrl.replace('/dashboard/', '/dashboard-solo/');
+      soloUrl = soloUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
       soloUrl = soloUrl.replace("&fullscreen", "");
       soloUrl = soloUrl.replace("&fullscreen", "");
 
 
       $scope.iframeHtml = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
       $scope.iframeHtml = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
 
 
-      $scope.imageUrl = soloUrl.replace('/dashboard-solo/', '/render/dashboard-solo/');
+      $scope.imageUrl = soloUrl.replace(config.appSubUrl + '/dashboard-solo/', config.appSubUrl + '/render/dashboard-solo/');
       $scope.imageUrl += '&width=1000';
       $scope.imageUrl += '&width=1000';
       $scope.imageUrl += '&height=500';
       $scope.imageUrl += '&height=500';
     };
     };

+ 24 - 1
public/app/features/dashboard/submenu/submenu.ts

@@ -1,6 +1,7 @@
 ///<reference path="../../../headers/common.d.ts" />
 ///<reference path="../../../headers/common.d.ts" />
 
 
 import angular from 'angular';
 import angular from 'angular';
+import _ from 'lodash';
 
 
 export class SubmenuCtrl {
 export class SubmenuCtrl {
   annotations: any;
   annotations: any;
@@ -8,7 +9,11 @@ export class SubmenuCtrl {
   dashboard: any;
   dashboard: any;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor(private $rootScope, private templateValuesSrv) {
+  constructor(private $rootScope,
+              private templateValuesSrv,
+              private templateSrv,
+              private dynamicDashboardSrv,
+              private $location) {
     this.annotations = this.dashboard.templating.list;
     this.annotations = this.dashboard.templating.list;
     this.variables = this.dashboard.templating.list;
     this.variables = this.dashboard.templating.list;
   }
   }
@@ -22,8 +27,26 @@ export class SubmenuCtrl {
     return this.templateValuesSrv.getValuesForTag(variable, tagKey);
     return this.templateValuesSrv.getValuesForTag(variable, tagKey);
   }
   }
 
 
+  updateUrlParamsWithCurrentVariables() {
+    // update url
+    var params = this.$location.search();
+    // remove variable params
+    _.each(params, function(value, key) {
+      if (key.indexOf('var-') === 0) {
+        delete params[key];
+      }
+    });
+
+    // add new values
+    this.templateSrv.fillVariableValuesForUrl(params);
+    // update url
+    this.$location.search(params);
+  }
+
   variableUpdated(variable) {
   variableUpdated(variable) {
     this.templateValuesSrv.variableUpdated(variable).then(() => {
     this.templateValuesSrv.variableUpdated(variable).then(() => {
+      this.dynamicDashboardSrv.update(this.dashboard);
+      this.updateUrlParamsWithCurrentVariables();
       this.$rootScope.$emit('template-variable-value-updated');
       this.$rootScope.$emit('template-variable-value-updated');
       this.$rootScope.$broadcast('refresh');
       this.$rootScope.$broadcast('refresh');
     });
     });

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

@@ -10,7 +10,7 @@ define([
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
 
 
-  module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer) {
+  module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer, $location) {
     var self = this;
     var self = this;
 
 
     this.init = function(dashboard) {
     this.init = function(dashboard) {
@@ -108,6 +108,13 @@ define([
         this.old_refresh = null;
         this.old_refresh = null;
       }
       }
 
 
+      // update url params
+      var urlParams = $location.search();
+      var urlRange = this.timeRangeForUrl();
+      urlParams.from = urlRange.from;
+      urlParams.to = urlRange.to;
+      $location.search(urlParams);
+
       $rootScope.appEvent('time-range-changed', this.time);
       $rootScope.appEvent('time-range-changed', this.time);
       $timeout(this.refreshDashboard, 0);
       $timeout(this.refreshDashboard, 0);
     };
     };

+ 19 - 1
public/app/features/panel/panel_ctrl.ts

@@ -8,6 +8,7 @@ import $ from 'jquery';
 const TITLE_HEIGHT = 25;
 const TITLE_HEIGHT = 25;
 const EMPTY_TITLE_HEIGHT = 9;
 const EMPTY_TITLE_HEIGHT = 9;
 const PANEL_PADDING = 5;
 const PANEL_PADDING = 5;
+const PANEL_BORDER = 2;
 
 
 import {Emitter} from 'app/core/core';
 import {Emitter} from 'app/core/core';
 
 
@@ -90,6 +91,23 @@ export class PanelCtrl {
     this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
     this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
     this.editModeInitiated = true;
     this.editModeInitiated = true;
     this.events.emit('init-edit-mode', null);
     this.events.emit('init-edit-mode', null);
+
+    var routeParams = this.$injector.get('$routeParams');
+    if (routeParams.editorTab) {
+      this.editorTabs.forEach((tab, i) => {
+        if (tab.title === routeParams.editorTab) {
+          this.editorTabIndex = i;
+        }
+      });
+    }
+  }
+
+  changeTab(newIndex) {
+    this.editorTabIndex = newIndex;
+    var route = this.$injector.get('$route');
+
+    route.current.params.editorTab = this.editorTabs[newIndex].title;
+    route.updateParams();
   }
   }
 
 
   addEditorTab(title, directiveFn, index?) {
   addEditorTab(title, directiveFn, index?) {
@@ -141,7 +159,7 @@ export class PanelCtrl {
       }
       }
     }
     }
 
 
-    this.height = this.containerHeight - (PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));
+    this.height = this.containerHeight - (PANEL_BORDER + PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));
   }
   }
 
 
   render(payload?) {
   render(payload?) {

+ 1 - 1
public/app/features/panel/panel_directive.ts

@@ -36,7 +36,7 @@ var panelTemplate = `
 
 
         <ul class="gf-tabs">
         <ul class="gf-tabs">
           <li class="gf-tabs-item" ng-repeat="tab in ::ctrl.editorTabs">
           <li class="gf-tabs-item" ng-repeat="tab in ::ctrl.editorTabs">
-            <a class="gf-tabs-link" ng-click="ctrl.editorTabIndex = $index" ng-class="{active: ctrl.editorTabIndex === $index}">
+            <a class="gf-tabs-link" ng-click="ctrl.changeTab($index)" ng-class="{active: ctrl.editorTabIndex === $index}">
               {{::tab.title}}
               {{::tab.title}}
             </a>
             </a>
           </li>
           </li>

+ 16 - 9
public/app/features/templating/templateSrv.js

@@ -42,6 +42,16 @@ function (angular, _) {
       return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
       return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
     }
     }
 
 
+    this.luceneFormat = function(value) {
+      if (typeof value === 'string') {
+        return luceneEscape(value);
+      }
+      var quotedValues = _.map(value, function(val) {
+        return '\"' + luceneEscape(val) + '\"';
+      });
+      return '(' + quotedValues.join(' OR ') + ')';
+    };
+
     this.formatValue = function(value, format, variable) {
     this.formatValue = function(value, format, variable) {
       // for some scopedVars there is no variable
       // for some scopedVars there is no variable
       variable = variable || {};
       variable = variable || {};
@@ -60,13 +70,7 @@ function (angular, _) {
           return '(' + escapedValues.join('|') + ')';
           return '(' + escapedValues.join('|') + ')';
         }
         }
         case "lucene": {
         case "lucene": {
-          if (typeof value === 'string') {
-            return luceneEscape(value);
-          }
-          var quotedValues = _.map(value, function(val) {
-            return '\"' + luceneEscape(val) + '\"';
-          });
-          return '(' + quotedValues.join(' OR ') + ')';
+          return this.luceneFormat(value, format, variable);
         }
         }
         case "pipe": {
         case "pipe": {
           if (typeof value === 'string') {
           if (typeof value === 'string') {
@@ -97,8 +101,11 @@ function (angular, _) {
       if (!str) {
       if (!str) {
         return false;
         return false;
       }
       }
-      var match = this._regex.exec(str);
-      return match && (match[1] === variableName || match[2] === variableName);
+
+      variableName = regexEscape(variableName);
+      var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
+      var match = findVarRegex.exec(str);
+      return match !== null;
     };
     };
 
 
     this.highlightVariablesAsHtml = function(str) {
     this.highlightVariablesAsHtml = function(str) {

+ 25 - 4
public/app/features/templating/templateValuesSrv.js

@@ -79,7 +79,6 @@ function (angular, _, kbn) {
         else if (variable.refresh === 1 || variable.refresh === 2) {
         else if (variable.refresh === 1 || variable.refresh === 2) {
           return self.updateOptions(variable).then(function() {
           return self.updateOptions(variable).then(function() {
             if (_.isEmpty(variable.current) && variable.options.length) {
             if (_.isEmpty(variable.current) && variable.options.length) {
-              console.log("setting current for %s", variable.name);
               self.setVariableValue(variable, variable.options[0]);
               self.setVariableValue(variable, variable.options[0]);
             }
             }
             lock.resolve();
             lock.resolve();
@@ -102,7 +101,10 @@ function (angular, _, kbn) {
       }
       }
 
 
       return promise.then(function() {
       return promise.then(function() {
-        var option = _.findWhere(variable.options, { text: urlValue });
+        var option = _.find(variable.options, function(op) {
+          return op.text === urlValue || op.value === urlValue;
+        });
+
         option = option || { text: urlValue, value: urlValue };
         option = option || { text: urlValue, value: urlValue };
 
 
         self.updateAutoInterval(variable);
         self.updateAutoInterval(variable);
@@ -244,15 +246,26 @@ function (angular, _, kbn) {
     this.validateVariableSelectionState = function(variable) {
     this.validateVariableSelectionState = function(variable) {
       if (!variable.current) {
       if (!variable.current) {
         if (!variable.options.length) { return; }
         if (!variable.options.length) { return; }
-        return self.setVariableValue(variable, variable.options[0], true);
+        return self.setVariableValue(variable, variable.options[0], false);
       }
       }
 
 
       if (_.isArray(variable.current.value)) {
       if (_.isArray(variable.current.value)) {
         self.selectOptionsForCurrentValue(variable);
         self.selectOptionsForCurrentValue(variable);
+        // updated selected value
+        var selected = {
+          value: _.map(_.filter(variable.options, {selected: true}), function(op) {
+            return op.value;
+          })
+        };
+        // if none pick first
+        if (selected.value.length === 0) {
+          selected = variable.options[0];
+        }
+        return self.setVariableValue(variable, selected, false);
       } else {
       } else {
         var currentOption = _.findWhere(variable.options, {text: variable.current.text});
         var currentOption = _.findWhere(variable.options, {text: variable.current.text});
         if (currentOption) {
         if (currentOption) {
-          return self.setVariableValue(variable, currentOption, true);
+          return self.setVariableValue(variable, currentOption, false);
         } else {
         } else {
           if (!variable.options.length) { return; }
           if (!variable.options.length) { return; }
           return self.setVariableValue(variable, variable.options[0]);
           return self.setVariableValue(variable, variable.options[0]);
@@ -313,6 +326,14 @@ function (angular, _, kbn) {
         var value = item.value || item.text;
         var value = item.value || item.text;
         var text = item.text || item.value;
         var text = item.text || item.value;
 
 
+        if (_.isNumber(value)) {
+          value = value.toString();
+        }
+
+        if (_.isNumber(text)) {
+          text = text.toString();
+        }
+
         if (regex) {
         if (regex) {
           matches = regex.exec(value);
           matches = regex.exec(value);
           if (!matches) { continue; }
           if (!matches) { continue; }

+ 0 - 9
public/app/partials/login.html

@@ -73,14 +73,5 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<div class="row" style="margin-top: 50px">
-			<div class="version-footer text-center small">
-				Grafana version: {{buildInfo.version}}, commit: {{buildInfo.commit}},
-				build date: {{buildInfo.buildstamp | date: 'yyyy-MM-dd HH:mm:ss' }}
-			</div>
-			<div class="version-footer text-center small" ng-show="buildInfo.hasUpdate">
-				<a class="external-link" target="_blank" href="http://grafana.org/download">New Grafana Version Available ({{buildInfo.latestVersion}})</a>
-			</div>
-		</div>
 	</div>
 	</div>
 </div>
 </div>

+ 0 - 1
public/app/partials/signup_step2.html

@@ -67,7 +67,6 @@
 			</form>
 			</form>
 		</div>
 		</div>
 
 
-
 	</div>
 	</div>
 </div>
 </div>
 
 

+ 11 - 3
public/app/plugins/datasource/elasticsearch/datasource.js

@@ -78,7 +78,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
         range[timeField]["format"] = "epoch_millis";
         range[timeField]["format"] = "epoch_millis";
       }
       }
 
 
-      var queryInterpolated = templateSrv.replace(queryString);
+      var queryInterpolated = templateSrv.replace(queryString, {}, 'lucene');
       var filter = { "bool": { "must": [{ "range": range }] } };
       var filter = { "bool": { "must": [{ "range": range }] } };
       var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } };
       var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } };
       var data = {
       var data = {
@@ -204,6 +204,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       });
       });
     };
     };
 
 
+    function escapeForJson(value) {
+      return value.replace(/\"/g, '\\"');
+    }
+
+    function luceneThenJsonFormat(value) {
+      return escapeForJson(templateSrv.luceneFormat(value));
+    }
+
     this.getFields = function(query) {
     this.getFields = function(query) {
       return this._get('/_mapping').then(function(res) {
       return this._get('/_mapping').then(function(res) {
         var fields = {};
         var fields = {};
@@ -246,7 +254,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       var header = this.getQueryHeader('count', range.from, range.to);
       var header = this.getQueryHeader('count', range.from, range.to);
       var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
       var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
 
 
-      esQuery = esQuery.replace("$lucene_query", queryDef.query || '*');
+      esQuery = esQuery.replace("$lucene_query", escapeForJson(queryDef.query || '*'));
       esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
       esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
       esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
       esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
       esQuery = header + '\n' + esQuery + '\n';
       esQuery = header + '\n' + esQuery + '\n';
@@ -260,7 +268,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
     };
     };
 
 
     this.metricFindQuery = function(query) {
     this.metricFindQuery = function(query) {
-      query = templateSrv.replace(query);
+      query = templateSrv.replace(query, {}, luceneThenJsonFormat);
       query = angular.fromJson(query);
       query = angular.fromJson(query);
       if (!query) {
       if (!query) {
         return $q.when([]);
         return $q.when([]);

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

@@ -70,9 +70,9 @@
 	</div>
 	</div>
 
 
 	<div ng-if="agg.type === 'filters'">
 	<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-inline offset-width-7" ng-repeat="filter in agg.settings.filters">
 			<div class="gf-form">
 			<div class="gf-form">
-				<label class="gf-form-item width-10">Query {{$index + 1}}</label>
+				<label class="gf-form-label 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()">
 				<input type="text" class="gf-form-input max-width-12" ng-model="filter.query" spellcheck='false' placeholder="Lucene query" ng-blur="onChangeInternal()">
 			</div>
 			</div>
 			<div class="gf-form">
 			<div class="gf-form">
@@ -88,7 +88,7 @@
 
 
 	<div ng-if="agg.type === 'geohash_grid'">
 	<div ng-if="agg.type === 'geohash_grid'">
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
-			<label class="gf-form-label">Precision</label>
+			<label class="gf-form-label width-10">Precision</label>
 			<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.precision" spellcheck='false' placeholder="3" ng-blur="onChangeInternal()">
 			<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>
 	</div>

+ 8 - 17
public/app/plugins/datasource/prometheus/datasource.ts

@@ -256,23 +256,14 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
     return this.renderTemplate(options.legendFormat, labelData) || '{}';
     return this.renderTemplate(options.legendFormat, labelData) || '{}';
   };
   };
 
 
-  this.renderTemplate = function(format, data) {
-    var originalSettings = _.templateSettings;
-    _.templateSettings = {
-      interpolate: /\{\{(.+?)\}\}/g
-    };
-
-    var template = _.template(templateSrv.replace(format));
-    var result;
-    try {
-      result = template(data);
-    } catch (e) {
-      result = null;
-    }
-
-    _.templateSettings = originalSettings;
-
-    return result;
+  this.renderTemplate = function(aliasPattern, aliasData) {
+    var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
+    return aliasPattern.replace(aliasRegex, function(match, g1) {
+      if (aliasData[g1]) {
+        return aliasData[g1];
+      }
+      return g1;
+    });
   };
   };
 
 
   this.getOriginalMetricName = function(labelData) {
   this.getOriginalMetricName = function(labelData) {

+ 4 - 0
public/app/plugins/datasource/prometheus/query_ctrl.ts

@@ -58,6 +58,10 @@ class PrometheusQueryCtrl extends QueryCtrl {
 
 
   updateLink() {
   updateLink() {
     var range = this.panelCtrl.range;
     var range = this.panelCtrl.range;
+    if (!range) {
+      return;
+    }
+
     var rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000);
     var rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000);
     var endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
     var endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
     var expr = {
     var expr = {

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

@@ -66,7 +66,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
 
 
         function getLegendHeight(panelHeight) {
         function getLegendHeight(panelHeight) {
           if (!panel.legend.show || panel.legend.rightSide) {
           if (!panel.legend.show || panel.legend.rightSide) {
-            return 2;
+            return 0;
           }
           }
 
 
           if (panel.legend.alignAsTable) {
           if (panel.legend.alignAsTable) {

+ 9 - 9
public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts

@@ -22,8 +22,8 @@ describe('GraphCtrl', function() {
   describe('msResolution with second resolution timestamps', function() {
   describe('msResolution with second resolution timestamps', function() {
     beforeEach(function() {
     beforeEach(function() {
       var data = [
       var data = [
-        { target: 'test.cpu1', datapoints: [[1234567890, 45], [1234567899, 60]]},
-        { target: 'test.cpu2', datapoints: [[1236547890, 55], [1234456709, 90]]}
+        { target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
+        { target: 'test.cpu2', datapoints: [[55, 1236547890], [90, 1234456709]]}
       ];
       ];
       ctx.ctrl.panel.tooltip.msResolution = false;
       ctx.ctrl.panel.tooltip.msResolution = false;
       ctx.ctrl.onDataReceived(data);
       ctx.ctrl.onDataReceived(data);
@@ -37,8 +37,8 @@ describe('GraphCtrl', function() {
   describe('msResolution with millisecond resolution timestamps', function() {
   describe('msResolution with millisecond resolution timestamps', function() {
     beforeEach(function() {
     beforeEach(function() {
       var data = [
       var data = [
-        { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]},
-        { target: 'test.cpu2', datapoints: [[1236547890001, 55], [1234456709000, 90]]}
+        { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
+        { target: 'test.cpu2', datapoints: [[55, 1236547890001], [90, 1234456709000]]}
       ];
       ];
       ctx.ctrl.panel.tooltip.msResolution = false;
       ctx.ctrl.panel.tooltip.msResolution = false;
       ctx.ctrl.onDataReceived(data);
       ctx.ctrl.onDataReceived(data);
@@ -52,8 +52,8 @@ describe('GraphCtrl', function() {
   describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
   describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
     beforeEach(function() {
     beforeEach(function() {
       var data = [
       var data = [
-        { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]},
-        { target: 'test.cpu2', datapoints: [[1236547890000, 55], [1234456709000, 90]]}
+        { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
+        { target: 'test.cpu2', datapoints: [[55, 1236547890000], [90, 1234456709000]]}
       ];
       ];
       ctx.ctrl.panel.tooltip.msResolution = false;
       ctx.ctrl.panel.tooltip.msResolution = false;
       ctx.ctrl.onDataReceived(data);
       ctx.ctrl.onDataReceived(data);
@@ -67,9 +67,9 @@ describe('GraphCtrl', function() {
   describe('msResolution with millisecond resolution timestamps in one of the series', function() {
   describe('msResolution with millisecond resolution timestamps in one of the series', function() {
     beforeEach(function() {
     beforeEach(function() {
       var data = [
       var data = [
-        { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]},
-        { target: 'test.cpu2', datapoints: [[1236547890010, 55], [1234456709000, 90]]},
-        { target: 'test.cpu3', datapoints: [[1236547890000, 65], [1234456709000, 120]]}
+        { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
+        { target: 'test.cpu2', datapoints: [[55, 1236547890010], [90, 1234456709000]]},
+        { target: 'test.cpu3', datapoints: [[65, 1236547890000], [120, 1234456709000]]}
       ];
       ];
       ctx.ctrl.panel.tooltip.msResolution = false;
       ctx.ctrl.panel.tooltip.msResolution = false;
       ctx.ctrl.onDataReceived(data);
       ctx.ctrl.onDataReceived(data);

+ 0 - 1
public/app/plugins/panel/singlestat/module.html

@@ -1,4 +1,3 @@
 <div class="singlestat-panel">
 <div class="singlestat-panel">
 
 
 </div>
 </div>
-<div class="clearfix"></div>

+ 3 - 2
public/app/plugins/panel/singlestat/module.ts

@@ -325,6 +325,9 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     }
     }
 
 
     function addGauge() {
     function addGauge() {
+      var width = elem.width();
+      var height = elem.height();
+
       ctrl.invalidGaugeRange = false;
       ctrl.invalidGaugeRange = false;
       if (panel.gauge.minValue > panel.gauge.maxValue) {
       if (panel.gauge.minValue > panel.gauge.maxValue) {
         ctrl.invalidGaugeRange = true;
         ctrl.invalidGaugeRange = true;
@@ -332,8 +335,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       }
       }
 
 
       var plotCanvas = $('<div></div>');
       var plotCanvas = $('<div></div>');
-      var width = elem.width();
-      var height = elem.height();
       var plotCss = {
       var plotCss = {
         top: '10px',
         top: '10px',
         margin: 'auto',
         margin: 'auto',

+ 4 - 0
public/sass/_variables.dark.scss

@@ -268,3 +268,7 @@ $checkboxImageUrl: '../img/checkbox.png';
 $card-background: linear-gradient(135deg, #2f2f2f, #262626);
 $card-background: linear-gradient(135deg, #2f2f2f, #262626);
 $card-background-hover: linear-gradient(135deg, #343434, #262626);
 $card-background-hover: linear-gradient(135deg, #343434, #262626);
 $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3);
 $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3);
+
+// footer
+$footer-link-color:   $gray-1;
+$footer-link-hover:   $gray-4;

+ 4 - 0
public/sass/_variables.light.scss

@@ -292,3 +292,7 @@ $checkboxImageUrl: '../img/checkbox_white.png';
 $card-background: linear-gradient(135deg, $gray-5, $gray-6);
 $card-background: linear-gradient(135deg, $gray-5, $gray-6);
 $card-background-hover: linear-gradient(135deg, $gray-6, $gray-7);
 $card-background-hover: linear-gradient(135deg, $gray-6, $gray-7);
 $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1);
 $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1);
+
+// footer
+$footer-link-color:   $gray-3;
+$footer-link-hover:   $dark-5;

+ 36 - 7
public/sass/components/_footer.scss

@@ -1,9 +1,38 @@
-.grafana-version-info {
-  position: absolute;
-  bottom: 2px;
-  left: 3px;
-  font-size: 80%;
-  color: darken($gray-1, 25%);
-  a { color: darken($gray-1, 25%); }
+.page-dashboard .footer {
+	display: none;
 }
 }
 
 
+.footer {
+  color: $footer-link-color;
+  padding: 5rem 0 1rem 0;
+  font-size: $font-size-xs;
+  width: 98%;  /* was causing horiz scrollbars - need to examine */
+
+  a {
+    color: $footer-link-color;
+
+    &:hover {
+      color: $footer-link-hover;
+    }
+  }
+
+  ul {
+    list-style: none;
+  }
+
+  li {
+    display: inline-block;
+    padding-right: 2px;
+    &:after {
+      content: ' | ';
+      padding-left: 2px;
+    }
+  }
+
+  li:last-child {
+    &:after {
+      padding-left: 0;
+      content: '';
+    }
+  }
+}

+ 0 - 1
public/sass/components/_panel_singlestat.scss

@@ -5,7 +5,6 @@
 }
 }
 
 
 .singlestat-panel-value-container {
 .singlestat-panel-value-container {
-  padding: 20px;
   display: table-cell;
   display: table-cell;
   vertical-align: middle;
   vertical-align: middle;
   text-align: center;
   text-align: center;

+ 1 - 1
public/sass/layout/_page.scss

@@ -4,7 +4,7 @@
 }
 }
 
 
 .main-view {
 .main-view {
-  height: 100%;
+  // height: 100%; REMOVED FOR FOOTER TRW
 }
 }
 
 
 .page-container {
 .page-container {

+ 3 - 3
public/test/core/time_series_specs.js

@@ -56,7 +56,7 @@ define([
       });
       });
     });
     });
 
 
-    describe('can detect if serie contains ms precision', function() {
+    describe('can detect if series contains ms precision', function() {
       var fakedata;
       var fakedata;
 
 
       beforeEach(function() {
       beforeEach(function() {
@@ -64,13 +64,13 @@ define([
       });
       });
 
 
       it('missing datapoint with ms precision', function() {
       it('missing datapoint with ms precision', function() {
-        fakedata.datapoints[0] = [1234567890000, 1337];
+        fakedata.datapoints[0] = [1337, 1234567890000];
         series = new TimeSeries(fakedata);
         series = new TimeSeries(fakedata);
         expect(series.isMsResolutionNeeded()).to.be(false);
         expect(series.isMsResolutionNeeded()).to.be(false);
       });
       });
 
 
       it('contains datapoint with ms precision', function() {
       it('contains datapoint with ms precision', function() {
-        fakedata.datapoints[0] = [1236547890001, 1337];
+        fakedata.datapoints[0] = [1337, 1236547890001];
         series = new TimeSeries(fakedata);
         series = new TimeSeries(fakedata);
         expect(series.isMsResolutionNeeded()).to.be(true);
         expect(series.isMsResolutionNeeded()).to.be(true);
       });
       });

+ 11 - 2
public/test/specs/templateSrv-specs.js

@@ -141,8 +141,8 @@ define([
       });
       });
 
 
       it('slash should be properly escaped in regex format', function() {
       it('slash should be properly escaped in regex format', function() {
-         var result = _templateSrv.formatValue('Gi3/14', 'regex');
-         expect(result).to.be('Gi3\\/14');
+        var result = _templateSrv.formatValue('Gi3/14', 'regex');
+        expect(result).to.be('Gi3\\/14');
       });
       });
 
 
     });
     });
@@ -200,6 +200,15 @@ define([
         expect(contains).to.be(true);
         expect(contains).to.be(true);
       });
       });
 
 
+      it('should find it when part of segment', function() {
+        var contains = _templateSrv.containsVariable('metrics.$env.$group-*', 'group');
+        expect(contains).to.be(true);
+      });
+
+      it('should find it its the only thing', function() {
+        var contains = _templateSrv.containsVariable('$env', 'env');
+        expect(contains).to.be(true);
+      });
     });
     });
 
 
     describe('updateTemplateData with simple value', function() {
     describe('updateTemplateData with simple value', function() {

+ 53 - 0
public/test/specs/templateValuesSrv-specs.js

@@ -126,6 +126,59 @@ define([
       });
       });
     });
     });
 
 
+    describeUpdateVariable('query variable with multi select and new options does not contain some selected values', function(scenario) {
+      scenario.setup(function() {
+        scenario.variable = {
+          type: 'query',
+          query: '',
+          name: 'test',
+          current: {
+            value: ['val1', 'val2', 'val3'],
+            text: 'val1 + val2 + val3'
+          }
+        };
+        scenario.queryResult = [{text: 'val2'}, {text: 'val3'}];
+      });
+
+      it('should update current value', function() {
+        expect(scenario.variable.current.value).to.eql(['val2', 'val3']);
+        expect(scenario.variable.current.text).to.eql('val2 + val3');
+      });
+    });
+
+    describeUpdateVariable('query variable with multi select and new options does not contain any selected values', function(scenario) {
+      scenario.setup(function() {
+        scenario.variable = {
+          type: 'query',
+          query: '',
+          name: 'test',
+          current: {
+            value: ['val1', 'val2', 'val3'],
+            text: 'val1 + val2 + val3'
+          }
+        };
+        scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
+      });
+
+      it('should update current value with first one', function() {
+        expect(scenario.variable.current.value).to.eql('val5');
+        expect(scenario.variable.current.text).to.eql('val5');
+      });
+    });
+
+    describeUpdateVariable('query variable with numeric results', function(scenario) {
+      scenario.setup(function() {
+        scenario.variable = { type: 'query', query: '', name: 'test', current: {} };
+        scenario.queryResult = [{text: 12, value: 12}];
+      });
+
+      it('should set current value to first option', function() {
+        expect(scenario.variable.current.value).to.be('12');
+        expect(scenario.variable.options[0].value).to.be('12');
+        expect(scenario.variable.options[0].text).to.be('12');
+      });
+    });
+
     describeUpdateVariable('interval variable without auto', function(scenario) {
     describeUpdateVariable('interval variable without auto', function(scenario) {
       scenario.setup(function() {
       scenario.setup(function() {
         scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' };
         scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' };

+ 35 - 0
public/views/index.html

@@ -39,6 +39,41 @@
 			</div>
 			</div>
 
 
 			<div ng-view class="main-view"></div>
 			<div ng-view class="main-view"></div>
+			<footer class="footer">
+				<div class="row text-center">
+					<ul>
+						<li>
+							<a href="http://docs.grafana.org" target="_blank">
+								<i class="fa fa-file-code-o"></i>
+								Docs
+							</a>
+						</li>
+						<li>
+							<a href="https://grafana.net/support/plans" target="_blank">
+								<i class="fa fa-support"></i>
+								Support Plans
+							</a>
+						</li>
+						<li>
+							<a href="https://grafana.org/community" target="_blank">
+								<i class="fa fa-comments-o"></i>
+								Community
+							</a>
+						</li>
+						<li>
+							<a href="http://grafana.org" target="_blank">Grafana</a>
+							<span>v[[.BuildVersion]] (commit: [[.BuildCommit]])</span>
+						</li>
+						<li>
+							[[if .NewGrafanaVersionExists]]
+								<a href="http://grafana.org/download" target="_blank" bs-tooltip="'[[.NewGrafanaVersion]]'">
+									New version available!
+								</a>
+							[[end]]
+						</li>
+					</ul>
+				</div>
+			</footer>
 		</grafana-app>
 		</grafana-app>
   </body>
   </body>
 
 

+ 2 - 2
vendor/phantomjs/render.js

@@ -38,10 +38,10 @@
     function checkIsReady() {
     function checkIsReady() {
       var canvas = page.evaluate(function() {
       var canvas = page.evaluate(function() {
         if (!window.angular) { return false; }
         if (!window.angular) { return false; }
-        var body = window.angular.element(document.body);   // 1
+        var body = window.angular.element(document.body);
         if (!body.scope) { return false; }
         if (!body.scope) { return false; }
 
 
-        var rootScope = body.scope();
+        var rootScope = body.injector().get('$rootScope');
         if (!rootScope) {return false;}
         if (!rootScope) {return false;}
         if (!rootScope.performance) { return false; }
         if (!rootScope.performance) { return false; }
         var panelsToLoad = window.angular.element('div.panel').length;
         var panelsToLoad = window.angular.element('div.panel').length;