Browse Source

Merge branch 'master' into alerting_definitions

bergquist 9 years ago
parent
commit
1686d86c3b
68 changed files with 768 additions and 219 deletions
  1. 6 7
      .github/ISSUE_TEMPLATE.md
  2. 22 2
      CHANGELOG.md
  3. 16 0
      docker/blocks/collectd/Dockerfile
  4. 37 0
      docker/blocks/collectd/README.md
  5. 76 0
      docker/blocks/collectd/collectd.conf.tpl
  6. 1 0
      docker/blocks/collectd/etc_mtab
  7. 11 0
      docker/blocks/collectd/fig
  8. 5 0
      docker/blocks/collectd/start_container
  9. 48 5
      docs/README.md
  10. 1 0
      docs/mkdocs.yml
  11. 1 1
      docs/sources/installation/configuration.md
  12. 3 3
      docs/sources/installation/debian.md
  13. 4 4
      docs/sources/installation/rpm.md
  14. 1 1
      docs/sources/installation/windows.md
  15. 2 2
      latest.json
  16. 1 1
      package.json
  17. 10 12
      packaging/publish/publish.sh
  18. 2 1
      pkg/api/api.go
  19. 6 5
      pkg/api/cloudwatch/cloudwatch.go
  20. 10 0
      pkg/api/cloudwatch/metrics.go
  21. 11 14
      pkg/api/dashboard.go
  22. 7 0
      pkg/api/dataproxy.go
  23. 11 7
      pkg/api/dtos/index.go
  24. 9 5
      pkg/api/index.go
  25. 1 1
      pkg/api/pluginproxy/pluginproxy.go
  26. 18 0
      pkg/api/user.go
  27. 1 1
      pkg/cmd/grafana-server/main.go
  28. 0 9
      public/app/core/controllers/login_ctrl.js
  29. 3 1
      public/app/core/directives/metric_segment.js
  30. 1 1
      public/app/core/directives/plugin_component.ts
  31. 5 0
      public/app/core/services/backend_srv.js
  32. 29 22
      public/app/core/services/datasource_srv.js
  33. 2 2
      public/app/core/time_series2.ts
  34. 18 5
      public/app/core/utils/kbn.js
  35. 1 1
      public/app/features/annotations/editor_ctrl.js
  36. 16 3
      public/app/features/dashboard/rowCtrl.js
  37. 2 2
      public/app/features/dashboard/shareModalCtrl.js
  38. 6 1
      public/app/features/dashboard/submenu/submenu.ts
  39. 35 7
      public/app/features/dashboard/viewStateSrv.js
  40. 2 1
      public/app/features/panel/panel_ctrl.ts
  41. 4 8
      public/app/features/panel/partials/soloPanel.html
  42. 4 0
      public/app/features/panel/solo_panel_ctrl.js
  43. 20 0
      public/app/features/templating/editorCtrl.js
  44. 9 1
      public/app/features/templating/partials/editor.html
  45. 16 9
      public/app/features/templating/templateSrv.js
  46. 41 8
      public/app/features/templating/templateValuesSrv.js
  47. 0 9
      public/app/partials/login.html
  48. 0 1
      public/app/partials/signup_step2.html
  49. 11 3
      public/app/plugins/datasource/elasticsearch/datasource.js
  50. 3 3
      public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html
  51. 8 17
      public/app/plugins/datasource/prometheus/datasource.ts
  52. 4 0
      public/app/plugins/datasource/prometheus/query_ctrl.ts
  53. 1 1
      public/app/plugins/panel/graph/graph.js
  54. 9 9
      public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
  55. 0 1
      public/app/plugins/panel/singlestat/module.html
  56. 3 2
      public/app/plugins/panel/singlestat/module.ts
  57. 4 0
      public/sass/_variables.dark.scss
  58. 4 0
      public/sass/_variables.light.scss
  59. 36 7
      public/sass/components/_footer.scss
  60. 0 1
      public/sass/components/_panel_singlestat.scss
  61. 1 1
      public/sass/layout/_page.scss
  62. 3 3
      public/test/core/time_series_specs.js
  63. 6 0
      public/test/core/utils/kbn_specs.js
  64. 15 3
      public/test/specs/dashboardViewStateSrv-specs.js
  65. 11 2
      public/test/specs/templateSrv-specs.js
  66. 74 0
      public/test/specs/templateValuesSrv-specs.js
  67. 35 0
      public/views/index.html
  68. 5 3
      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:
-- What grafana version are you using?
+- What Grafana version are you using?
 - What datasource are you using?
 - What OS are you running grafana on?
 - What did you do?
 - What was the expected result?
 - 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
 - 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
+* **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)
 * **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)
+* **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)
 * **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)
 

+ 16 - 0
docker/blocks/collectd/Dockerfile

@@ -0,0 +1,16 @@
+FROM    ubuntu:xenial
+
+ENV     DEBIAN_FRONTEND noninteractive
+
+RUN     apt-get -y update
+RUN     apt-get -y install collectd curl python-pip
+
+# add a fake mtab for host disk stats
+ADD     etc_mtab /etc/mtab
+
+ADD     collectd.conf.tpl /etc/collectd/collectd.conf.tpl
+
+RUN	pip install envtpl
+ADD     start_container /usr/bin/start_container
+RUN     chmod +x /usr/bin/start_container
+CMD     start_container

+ 37 - 0
docker/blocks/collectd/README.md

@@ -0,0 +1,37 @@
+collectd-write-graphite
+=======================
+
+Basic collectd-based server monitoring. Sends stats to Graphite.
+
+Collectd metrics:
+
+* CPU used/free/idle/etc
+* Free disk (via mounting hosts '/' into container, eg: -v /:/hostfs:ro)
+* Disk performance
+* Load average
+* Memory used/free/etc
+* Uptime
+* Network interface
+* Swap
+
+Environment variables
+---------------------
+
+* `HOST_NAME`
+  - Will be sent to Graphite
+  - Required
+* `GRAPHITE_HOST`
+  - Graphite IP or hostname
+  - Required
+* `GRAPHITE_PORT`
+  - Graphite port
+  - Optional, defaults to 2003
+* `GRAPHITE_PREFIX`
+  - Graphite prefix
+  - Optional, defaults to collectd.
+* `REPORT_BY_CPU`
+  - Report per-CPU metrics if true, global sum of CPU metrics if false (details: [collectd.conf man page](https://collectd.org/documentation/manpages/collectd.conf.5.shtml#plugin_cpu))
+  - Optional, defaults to false.
+* `COLLECT_INTERVAL`
+  - Collection interval and thus resolution of metrics
+  - Optional, defaults to 10

+ 76 - 0
docker/blocks/collectd/collectd.conf.tpl

@@ -0,0 +1,76 @@
+Hostname "{{ HOST_NAME }}"
+
+FQDNLookup false
+Interval {{ COLLECT_INTERVAL | default("10") }}
+Timeout 2
+ReadThreads 5
+
+LoadPlugin cpu
+LoadPlugin df
+LoadPlugin load
+LoadPlugin memory
+LoadPlugin disk
+LoadPlugin interface
+LoadPlugin uptime
+LoadPlugin swap
+LoadPlugin write_graphite
+
+<Plugin cpu>
+  ReportByCpu {{ REPORT_BY_CPU | default("false") }}
+</Plugin>
+
+<Plugin df>
+  # expose host's mounts into container using -v /:/host:ro  (location inside container does not matter much)
+  # ignore rootfs; else, the root file-system would appear twice, causing
+  # one of the updates to fail and spam the log
+  FSType rootfs
+  # ignore the usual virtual / temporary file-systems
+  FSType sysfs
+  FSType proc
+  FSType devtmpfs
+  FSType devpts
+  FSType tmpfs
+  FSType fusectl
+  FSType cgroup
+  FSType overlay
+  FSType debugfs
+  FSType pstore
+  FSType securityfs
+  FSType hugetlbfs
+  FSType squashfs
+  FSType mqueue
+  MountPoint "/etc/resolv.conf"
+  MountPoint "/etc/hostname"
+  MountPoint "/etc/hosts"
+  IgnoreSelected true
+  ReportByDevice false
+  ReportReserved true
+  ReportInodes true
+</Plugin>
+
+<Plugin "disk">
+  Disk "/^[hs]d[a-z]/"
+  IgnoreSelected false
+</Plugin>
+
+
+<Plugin interface>
+  Interface "lo"
+  Interface "/^veth.*/"
+  Interface "/^docker.*/"
+  IgnoreSelected true
+</Plugin>
+
+
+<Plugin "write_graphite">
+ <Carbon>
+   Host "{{ GRAPHITE_HOST }}"
+   Port "{{ GRAPHITE_PORT | default("2003") }}"
+   Prefix "{{ GRAPHITE_PREFIX | default("collectd.") }}"
+   EscapeCharacter "_"
+   SeparateInstances true
+   StoreRates true
+   AlwaysAppendDS false
+ </Carbon>
+</Plugin>
+

+ 1 - 0
docker/blocks/collectd/etc_mtab

@@ -0,0 +1 @@
+hostfs /.dockerinit ext4 ro,relatime,user_xattr,barrier=1,data=ordered 0 0

+ 11 - 0
docker/blocks/collectd/fig

@@ -0,0 +1,11 @@
+collectd:
+  build: blocks/collectd
+  environment:
+    HOST_NAME: myserver
+    GRAPHITE_HOST: graphite
+    GRAPHITE_PORT: 2003
+    GRAPHITE_PREFIX: collectd.
+    REPORT_BY_CPU: 'false'
+    COLLECT_INTERVAL: 10
+  links:
+    - graphite

+ 5 - 0
docker/blocks/collectd/start_container

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+envtpl /etc/collectd/collectd.conf.tpl
+
+collectd -f

+ 48 - 5
docs/README.md

@@ -1,7 +1,15 @@
-To build the docs locally, you need to have docker installed.  The docs are built using a custom [docker](https://www.docker.com/)
-image and [mkdocs](http://www.mkdocs.org/).
+# Building The Docs
 
-Build the `grafana/docs-base:latest` image:
+To build the docs locally, you need to have docker installed.  The
+docs are built using a custom [docker](https://www.docker.com/) image
+and the [mkdocs](http://www.mkdocs.org/) tool.
+
+**Prepare the Docker Image**:
+
+Build the `grafana/docs-base:latest` image. Run these commands in the
+same directory this file is in. **Note** that you may require ``sudo``
+when running ``make docs-build`` depending on how your system's docker
+service is configured):
 
 ```
 $ git clone https://github.com/grafana/docs-base
@@ -9,10 +17,45 @@ $ cd docs-base
 $ make docs-build
 ```
 
-To build the docs:
+**Build the Documentation**:
+
+Now that the docker image has been prepared we can build the
+docs. Switch your working directory back to the directory this file
+(README.md) is in and run (possibly with ``sudo``):
+
 ```
-$ cd docs
 $ make docs
 ```
 
+This command will not return control of the shell to the user. Instead
+the command is now running a new docker container built from the image
+we created in the previous step.
+
 Open [localhost:8180](http://localhost:8180) to view the docs.
+
+**Note** that after running ``make docs`` you may notice a message
+like this in the console output
+
+> Running at: http://0.0.0.0:8000/
+
+This is misleading. That is **not** the port the documentation is
+served from. You must browse to port **8180** to view the new
+documentation.
+
+
+# Adding a New Page
+
+Adding a new page requires updating the ``mkdocs.yml`` file which is
+located in this directory.
+
+For example, if you are adding documentation for a new HTTP API called
+``preferences`` you would:
+
+1. Create the file ``docs/sources/http_api/preferences.md``
+1. Add a reference to it in ``docs/sources/http_api/overview.md``
+1. Update the list under the **pages** key in the ``docs/mkdocs.yml`` file with a reference to your new page:
+
+
+```yaml
+- ['http_api/preferences.md', 'API', 'Preferences API']
+```

+ 1 - 0
docs/mkdocs.yml

@@ -84,6 +84,7 @@ pages:
 - ['http_api/user.md', 'API', 'User API']
 - ['http_api/admin.md', 'API', 'Admin API']
 - ['http_api/snapshot.md', 'API', 'Snapshot API']
+- ['http_api/preferences.md', 'API', 'Preferences API']
 - ['http_api/other.md', 'API', 'Other API']
 
 - ['plugins/index.md', 'Plugins', 'Overview']

+ 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
 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>
 

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

@@ -10,13 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
 
 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
 
-    $ 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 dpkg -i grafana_3.0.2-1463383025_amd64.deb
+    $ sudo dpkg -i grafana_3.0.4-1464167696_amd64.deb
 
 ## APT Repository
 

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

@@ -10,24 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
 
 Description | Download
 ------------ | -------------
-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
 
 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`.
 
 #### On CentOS / Fedora / Redhat:
 
     $ 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:
 
-    $ 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
 

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

@@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
 
 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
 

+ 2 - 2
latest.json

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

+ 1 - 1
package.json

@@ -20,7 +20,7 @@
     "grunt-angular-templates": "^0.5.5",
     "grunt-cli": "~0.1.13",
     "grunt-contrib-clean": "~0.7.0",
-    "grunt-contrib-compress": "~0.14.0",
+    "grunt-contrib-compress": "^1.3.0",
     "grunt-contrib-concat": "^0.5.1",
     "grunt-contrib-copy": "~0.8.2",
     "grunt-contrib-cssmin": "~0.14.0",

+ 10 - 12
packaging/publish/publish.sh

@@ -1,22 +1,20 @@
 #! /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/6 grafana-${rpm_ver}.x86_64.rpm

+ 2 - 1
pkg/api/api.go

@@ -117,6 +117,7 @@ func Register(r *macaron.Macaron) {
 			r.Get("/:id", wrap(GetUserById))
 			r.Get("/:id/orgs", wrap(GetUserOrgList))
 			r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
+			r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
 		}, reqGrafanaAdmin)
 
 		// org information available to all users.
@@ -211,7 +212,7 @@ func Register(r *macaron.Macaron) {
 			r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
 			r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard)
 			r.Get("/file/:file", GetDashboardFromJsonFile)
-			r.Get("/home", GetHomeDashboard)
+			r.Get("/home", wrap(GetHomeDashboard))
 			r.Get("/tags", GetDashboardTags)
 			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
 
 func getCredentials(profile string, region string, assumeRoleArn string) *credentials.Credentials {
+	cacheKey := profile + ":" + assumeRoleArn
 	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()
 			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},
 		})
 	credentialCacheLock.Lock()
-	awsCredentialCache[profile] = cache{
+	awsCredentialCache[cacheKey] = cache{
 		credential: creds,
 		expiration: expiration,
 	}

+ 10 - 0
pkg/api/cloudwatch/metrics.go

@@ -45,6 +45,15 @@ func init() {
 		"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"},
 		"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
 		"AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"},
+		"AWS/ElasticBeanstalk": {
+			"EnvironmentHealth",
+			"ApplicationLatencyP10", "ApplicationLatencyP50", "ApplicationLatencyP75", "ApplicationLatencyP85", "ApplicationLatencyP90", "ApplicationLatencyP95", "ApplicationLatencyP99", "ApplicationLatencyP99.9",
+			"ApplicationRequests2xx", "ApplicationRequests3xx", "ApplicationRequests4xx", "ApplicationRequests5xx", "ApplicationRequestsTotal",
+			"CPUIdle", "CPUIowait", "CPUIrq", "CPUNice", "CPUSoftirq", "CPUSystem", "CPUUser",
+			"InstanceHealth", "InstancesDegraded", "InstancesInfo", "InstancesNoData", "InstancesOk", "InstancesPending", "InstancesSevere", "InstancesUnknown", "InstancesWarning",
+			"LoadAverage1min", "LoadAverage5min",
+			"RootFilesystemUtil",
+		},
 		"AWS/ElasticMapReduce": {"IsIdle", "JobsRunning", "JobsFailed",
 			"MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen",
 			"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers",
@@ -85,6 +94,7 @@ func init() {
 		"AWS/EBS":              {"VolumeId"},
 		"AWS/EC2":              {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
 		"AWS/ELB":              {"LoadBalancerName", "AvailabilityZone"},
+		"AWS/ElasticBeanstalk": {"EnvironmentName", "InstanceId"},
 		"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
 		"AWS/ES":               {"ClientId", "DomainName"},
 		"AWS/Events":           {"RuleName"},

+ 11 - 14
pkg/api/dashboard.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
@@ -174,30 +175,27 @@ func canEditDashboard(role m.RoleType) bool {
 	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}
 	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 {
 		slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
 		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")
 	file, err := os.Open(filePath)
 	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{}
@@ -205,11 +203,10 @@ func GetHomeDashboard(c *middleware.Context) {
 	dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
 	jsonParser := json.NewDecoder(file)
 	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) {

+ 7 - 0
pkg/api/dataproxy.go

@@ -55,6 +55,13 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *ht
 			req.Header.Add("Authorization", util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword))
 		}
 
+		dsAuth := req.Header.Get("X-DS-Authorization")
+		if len(dsAuth) > 0 {
+			req.Header.Del("X-DS-Authorization")
+			req.Header.Del("Authorization")
+			req.Header.Add("Authorization", dsAuth)
+		}
+
 		// clear cookie headers
 		req.Header.Del("Cookie")
 		req.Header.Del("Set-Cookie")

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

@@ -1,13 +1,17 @@
 package dtos
 
 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 {

+ 9 - 5
pkg/api/index.go

@@ -36,11 +36,15 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 			LightTheme:     prefs.Theme == "light",
 			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 {

+ 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 {
-				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])
 			}
 		}

+ 18 - 0
pkg/api/user.go

@@ -40,6 +40,24 @@ func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
 	return handleUpdateUser(cmd)
 }
 
+//POST /api/users/:id/using/:orgId
+func UpdateUserActiveOrg(c *middleware.Context) Response {
+	userId := c.ParamsInt64(":id")
+	orgId := c.ParamsInt64(":orgId")
+
+	if !validateUsingOrg(userId, orgId) {
+		return ApiError(401, "Not a valid organization", nil)
+	}
+
+	cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed change active organization", err)
+	}
+
+	return ApiSuccess("Active organization changed")
+}
+
 func handleUpdateUser(cmd m.UpdateUserCommand) Response {
 	if len(cmd.Login) == 0 {
 		cmd.Login = cmd.Email

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

@@ -25,7 +25,7 @@ import (
 	"github.com/grafana/grafana/pkg/social"
 )
 
-var version = "3.0.0-beta4"
+var version = "3.1.0"
 var commit = "NA"
 var buildstamp 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() {
       if ($scope.loginMode) {
         $scope.login();

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

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

+ 1 - 1
public/app/core/directives/plugin_component.ts

@@ -244,7 +244,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
         registerPluginComponent(scope, elem, attrs, componentInfo);
       }).catch(err => {
         $rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]);
-        console.log('Plugin componnet error', err);
+        console.log('Plugin component error', err);
       });
     }
   };

+ 5 - 0
public/app/core/services/backend_srv.js

@@ -96,6 +96,11 @@ function (angular, _, coreModule, config) {
       var requestIsLocal = options.url.indexOf('/') === 0;
       var firstAttempt = options.retry === 0;
 
+      if (requestIsLocal && options.headers && options.headers.Authorization) {
+        options.headers['X-DS-Authorization'] = options.headers.Authorization;
+        delete options.headers.Authorization;
+      }
+
       return $http(options).then(null, function(err) {
         // handle unauthorized for backend requests
         if (requestIsLocal && firstAttempt  && err.status === 401) {

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

@@ -66,14 +66,17 @@ function (angular, _, coreModule, config) {
     };
 
     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) {
-          memo.push(value);
+          sources.push(value);
         }
+      });
 
-        return memo;
-      }, []);
+      return sources;
     };
 
     this.getMetricSources = function(options) {
@@ -90,24 +93,7 @@ function (angular, _, coreModule, config) {
       });
 
       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) {
@@ -123,6 +109,27 @@ function (angular, _, coreModule, config) {
       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();
   });
 });

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

@@ -173,8 +173,8 @@ export default class TimeSeries {
 
   isMsResolutionNeeded() {
     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) {
           return true;
         }

+ 18 - 5
public/app/core/utils/kbn.js

@@ -12,9 +12,21 @@ function($, _) {
 
   kbn.round_interval = function(interval) {
     switch (true) {
-    // 0.3s
-    case (interval <= 300):
-      return 100;       // 0.1s
+    // 0.015s
+    case (interval <= 15):
+      return 10;      // 0.01s
+    // 0.035s
+    case (interval <= 35):
+      return 20;      // 0.02s
+    // 0.075s
+    case (interval <= 75):
+      return 50;       // 0.05s
+    // 0.15s
+    case (interval <= 150):
+      return 100;      // 0.1s
+    // 0.35s
+    case (interval <= 350):
+      return 200;      // 0.2s
     // 0.75s
     case (interval <= 750):
       return 500;       // 0.5s
@@ -133,7 +145,7 @@ function($, _) {
     return str;
   };
 
-  kbn.interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/;
+  kbn.interval_regex = /(\d+(?:\.\d+)?)(ms|[Mwdhmsy])/;
 
   // histogram & trends
   kbn.intervals_in_seconds = {
@@ -143,7 +155,8 @@ function($, _) {
     d: 86400,
     h: 3600,
     m: 60,
-    s: 1
+    s: 1,
+    ms: 0.001
   };
 
   kbn.calculateInterval = function(range, resolution, userInterval) {

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

@@ -30,7 +30,7 @@ function (angular, _, $) {
     $scope.datasourceChanged = function() {
       return datasourceSrv.get($scope.currentAnnotation.datasource).then(function(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() {
+
     return function(scope, element) {
+      var fullscreen = false;
+
       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) {
+        fullscreen = true;
+
         if (scope.panel.id !== info.panelId) {
           element.hide();
         } else {
@@ -156,14 +163,20 @@ function (angular, _, config) {
       });
 
       scope.onAppEvent('panel-fullscreen-exit', function(evt, info) {
+        fullscreen = false;
+
         if (scope.panel.id !== info.panelId) {
           element.show();
-        } else {
-          updateWidth();
         }
+
+        updateWidth();
       });
 
       scope.$watch('panel.span', updateWidth);
+
+      if (fullscreen) {
+        element.hide();
+      }
     };
   });
 

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

@@ -70,12 +70,12 @@ function (angular, _, require, config) {
       $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
 
       var soloUrl = $scope.shareUrl;
-      soloUrl = soloUrl.replace('/dashboard/', '/dashboard-solo/');
+      soloUrl = soloUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
       soloUrl = soloUrl.replace("&fullscreen", "");
 
       $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 += '&height=500';
     };

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

@@ -1,6 +1,7 @@
 ///<reference path="../../../headers/common.d.ts" />
 
 import angular from 'angular';
+import _ from 'lodash';
 
 export class SubmenuCtrl {
   annotations: any;
@@ -8,7 +9,11 @@ export class SubmenuCtrl {
   dashboard: any;
 
   /** @ngInject */
-  constructor(private $rootScope, private templateValuesSrv, private dynamicDashboardSrv) {
+  constructor(private $rootScope,
+              private templateValuesSrv,
+              private templateSrv,
+              private dynamicDashboardSrv,
+              private $location) {
     this.annotations = this.dashboard.templating.list;
     this.variables = this.dashboard.templating.list;
   }

+ 35 - 7
public/app/features/dashboard/viewStateSrv.js

@@ -8,7 +8,7 @@ function (angular, _, $) {
 
   var module = angular.module('grafana.services');
 
-  module.factory('dashboardViewStateSrv', function($location, $timeout) {
+  module.factory('dashboardViewStateSrv', function($location, $timeout, templateSrv, contextSrv, timeSrv) {
 
     // represents the transient view state
     // like fullscreen panel & edit
@@ -25,6 +25,19 @@ function (angular, _, $) {
         }
       };
 
+      // update url on time range change
+      $scope.onAppEvent('time-range-changed', function() {
+        var urlParams = $location.search();
+        var urlRange = timeSrv.timeRangeForUrl();
+        urlParams.from = urlRange.from;
+        urlParams.to = urlRange.to;
+        $location.search(urlParams);
+      });
+
+      $scope.onAppEvent('template-variable-value-updated', function() {
+        self.updateUrlParamsWithCurrentVariables();
+      });
+
       $scope.onAppEvent('$routeUpdate', function() {
         var urlState = self.getQueryStringState();
         if (self.needsSync(urlState)) {
@@ -40,10 +53,26 @@ function (angular, _, $) {
         self.registerPanel(payload.scope);
       });
 
-      this.update(this.getQueryStringState(), true);
+      this.update(this.getQueryStringState());
       this.expandRowForPanel();
     }
 
+    DashboardViewState.prototype.updateUrlParamsWithCurrentVariables = function() {
+      // update url
+      var params = $location.search();
+      // remove variable params
+      _.each(params, function(value, key) {
+        if (key.indexOf('var-') === 0) {
+          delete params[key];
+        }
+      });
+
+      // add new values
+      templateSrv.fillVariableValuesForUrl(params);
+      // update url
+      $location.search(params);
+    };
+
     DashboardViewState.prototype.expandRowForPanel = function() {
       if (!this.state.panelId) { return; }
 
@@ -63,6 +92,7 @@ function (angular, _, $) {
       state.fullscreen = state.fullscreen ? true : null;
       state.edit =  (state.edit === "true" || state.edit === true) || null;
       state.editview = state.editview || null;
+      state.org = contextSrv.user.orgId;
       return state;
     };
 
@@ -70,10 +100,11 @@ function (angular, _, $) {
       var urlState = _.clone(this.state);
       urlState.fullscreen = this.state.fullscreen ? true : null;
       urlState.edit = this.state.edit ? true : null;
+      urlState.org = contextSrv.user.orgId;
       return urlState;
     };
 
-    DashboardViewState.prototype.update = function(state, skipUrlSync) {
+    DashboardViewState.prototype.update = function(state) {
       _.extend(this.state, state);
       this.dashboard.meta.fullscreen = this.state.fullscreen;
 
@@ -83,10 +114,7 @@ function (angular, _, $) {
         this.state.edit = null;
       }
 
-      if (!skipUrlSync) {
-        $location.search(this.serializeToUrl());
-      }
-
+      $location.search(this.serializeToUrl());
       this.syncState();
     };
 

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

@@ -8,6 +8,7 @@ import $ from 'jquery';
 const TITLE_HEIGHT = 25;
 const EMPTY_TITLE_HEIGHT = 9;
 const PANEL_PADDING = 5;
+const PANEL_BORDER = 2;
 
 import {Emitter} from 'app/core/core';
 
@@ -158,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?) {

+ 4 - 8
public/app/features/panel/partials/soloPanel.html

@@ -1,9 +1,5 @@
-<div class="main">
-	<div class="row-fluid">
-		<div class="span12">
-			<div class="panel nospace" ng-if="panel" style="width: 100%">
-				<plugin-component type="panel">
-				</plugin-component>
-			</div>
-		</div>
+<div class="panel nospace" ng-if="panel" style="width: 100%">
+	<plugin-component type="panel">
+	</plugin-component>
 </div>
+<div class="clearfix"></div>

+ 4 - 0
public/app/features/panel/solo_panel_ctrl.js

@@ -17,6 +17,10 @@ function (angular, $) {
       var params = $location.search();
       panelId = parseInt(params.panelId);
 
+      // add fullscreen param;
+      params.fullscreen = true;
+      $location.search(params);
+
       dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) {
         $scope.initDashboard(result, $scope);
       });

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

@@ -25,6 +25,7 @@ function (angular, _) {
       {value: "interval",   text: "Interval"},
       {value: "datasource", text: "Data source"},
       {value: "custom",     text: "Custom"},
+      {value: "constant",   text: "Constant"},
     ];
 
     $scope.refreshOptions = [
@@ -141,15 +142,34 @@ function (angular, _) {
       $scope.current = angular.copy(replacementDefaults);
     };
 
+    $scope.showSelectionOptions = function() {
+      if ($scope.current) {
+        if ($scope.current.type === 'query') {
+          return true;
+        }
+        if ($scope.current.type === 'custom') {
+          return true;
+        }
+      }
+      return false;
+    };
+
     $scope.typeChanged = function () {
       if ($scope.current.type === 'interval') {
         $scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
+        $scope.current.refresh = 0;
       }
 
       if ($scope.current.type === 'query') {
         $scope.current.query = '';
       }
 
+      if ($scope.current.type === 'constant') {
+        $scope.current.query = '';
+        $scope.current.refresh = 0;
+        $scope.current.hide = 2;
+      }
+
       if ($scope.current.type === 'datasource') {
         $scope.current.query = $scope.datasourceTypes[0].value;
         $scope.current.regex = '';

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

@@ -152,6 +152,14 @@
 				</div>
 			</div>
 
+			<div ng-show="current.type === 'constant'" class="gf-form-group">
+        <h5 class="section-heading">Constant options</h5>
+				<div class="gf-form">
+					<span class="gf-form-label">Value</span>
+					<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="your metric prefix"></input>
+				</div>
+			</div>
+
 			<div ng-show="current.type === 'query'" class="gf-form-group">
         <h5 class="section-heading">Query Options</h5>
 
@@ -214,7 +222,7 @@
         </div>
       </div>
 
-      <div class="section gf-form-group" ng-hide="current.type === 'datasource'">
+      <div class="section gf-form-group" ng-show="showSelectionOptions()">
         <h5 class="section-heading">Selection Options</h5>
         <div class="section">
           <gf-form-switch class="gf-form"

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

@@ -42,6 +42,16 @@ function (angular, _) {
       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) {
       // for some scopedVars there is no variable
       variable = variable || {};
@@ -60,13 +70,7 @@ function (angular, _) {
           return '(' + escapedValues.join('|') + ')';
         }
         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": {
           if (typeof value === 'string') {
@@ -97,8 +101,11 @@ function (angular, _) {
       if (!str) {
         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) {

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

@@ -79,7 +79,6 @@ function (angular, _, kbn) {
         else if (variable.refresh === 1 || variable.refresh === 2) {
           return self.updateOptions(variable).then(function() {
             if (_.isEmpty(variable.current) && variable.options.length) {
-              console.log("setting current for %s", variable.name);
               self.setVariableValue(variable, variable.options[0]);
             }
             lock.resolve();
@@ -102,7 +101,10 @@ function (angular, _, kbn) {
       }
 
       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 };
 
         self.updateAutoInterval(variable);
@@ -125,8 +127,8 @@ function (angular, _, kbn) {
     this.setVariableValue = function(variable, option, initPhase) {
       variable.current = angular.copy(option);
 
-      if (_.isArray(variable.current.value)) {
-        variable.current.text = variable.current.value.join(' + ');
+      if (_.isArray(variable.current.text)) {
+        variable.current.text = variable.current.text.join(' + ');
       }
 
       self.selectOptionsForCurrentValue(variable);
@@ -166,6 +168,11 @@ function (angular, _, kbn) {
         return;
       }
 
+      if (variable.type === 'constant') {
+        variable.options = [{text: variable.query, value: variable.query}];
+        return;
+      }
+
       // extract options in comma seperated string
       variable.options = _.map(variable.query.split(/[,]+/), function(text) {
         return { text: text.trim(), value: text.trim() };
@@ -173,6 +180,7 @@ function (angular, _, kbn) {
 
       if (variable.type === 'interval') {
         self.updateAutoInterval(variable);
+        return;
       }
 
       if (variable.type === 'custom' && variable.includeAll) {
@@ -224,6 +232,7 @@ function (angular, _, kbn) {
 
     this.selectOptionsForCurrentValue = function(variable) {
       var i, y, value, option;
+      var selected = [];
 
       for (i = 0; i < variable.options.length; i++) {
         option = variable.options[i];
@@ -233,28 +242,44 @@ function (angular, _, kbn) {
             value = variable.current.value[y];
             if (option.value === value) {
               option.selected = true;
+              selected.push(option);
             }
           }
         } else if (option.value === variable.current.value) {
           option.selected = true;
+          selected.push(option);
         }
       }
+
+      return selected;
     };
 
     this.validateVariableSelectionState = function(variable) {
       if (!variable.current) {
         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)) {
-        self.selectOptionsForCurrentValue(variable);
+        var selected = self.selectOptionsForCurrentValue(variable);
+
+        // if none pick first
+        if (selected.length === 0) {
+          selected = variable.options[0];
+        } else {
+          selected = {
+            value: _.map(selected, function(val) {return val.value;}),
+            text: _.map(selected, function(val) {return val.text;}).join(' + '),
+          };
+        }
+
+        return self.setVariableValue(variable, selected, false);
       } else {
         var currentOption = _.findWhere(variable.options, {text: variable.current.text});
         if (currentOption) {
-          return self.setVariableValue(variable, currentOption, true);
+          return self.setVariableValue(variable, currentOption, false);
         } else {
-          if (!variable.options.length) { return; }
+          if (!variable.options.length) { return $q.when(null); }
           return self.setVariableValue(variable, variable.options[0]);
         }
       }
@@ -313,6 +338,14 @@ function (angular, _, kbn) {
         var value = item.value || item.text;
         var text = item.text || item.value;
 
+        if (_.isNumber(value)) {
+          value = value.toString();
+        }
+
+        if (_.isNumber(text)) {
+          text = text.toString();
+        }
+
         if (regex) {
           matches = regex.exec(value);
           if (!matches) { continue; }

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

@@ -73,14 +73,5 @@
 			</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>

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

@@ -67,7 +67,6 @@
 			</form>
 		</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";
       }
 
-      var queryInterpolated = templateSrv.replace(queryString);
+      var queryInterpolated = templateSrv.replace(queryString, {}, 'lucene');
       var filter = { "bool": { "must": [{ "range": range }] } };
       var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } };
       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) {
       return this._get('/_mapping').then(function(res) {
         var fields = {};
@@ -246,7 +254,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
       var header = this.getQueryHeader('count', range.from, range.to);
       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(/\$timeTo/g, range.to.valueOf());
       esQuery = header + '\n' + esQuery + '\n';
@@ -260,7 +268,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
     };
 
     this.metricFindQuery = function(query) {
-      query = templateSrv.replace(query);
+      query = templateSrv.replace(query, {}, luceneThenJsonFormat);
       query = angular.fromJson(query);
       if (!query) {
         return $q.when([]);

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

@@ -70,9 +70,9 @@
 	</div>
 
 	<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">
-				<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()">
 			</div>
 			<div class="gf-form">
@@ -88,7 +88,7 @@
 
 	<div ng-if="agg.type === 'geohash_grid'">
 		<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()">
 		</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) || '{}';
   };
 
-  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) {

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

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

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

@@ -66,7 +66,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
 
         function getLegendHeight(panelHeight) {
           if (!panel.legend.show || panel.legend.rightSide) {
-            return 2;
+            return 0;
           }
 
           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() {
     beforeEach(function() {
       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.onDataReceived(data);
@@ -37,8 +37,8 @@ describe('GraphCtrl', function() {
   describe('msResolution with millisecond resolution timestamps', function() {
     beforeEach(function() {
       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.onDataReceived(data);
@@ -52,8 +52,8 @@ describe('GraphCtrl', function() {
   describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
     beforeEach(function() {
       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.onDataReceived(data);
@@ -67,9 +67,9 @@ describe('GraphCtrl', function() {
   describe('msResolution with millisecond resolution timestamps in one of the series', function() {
     beforeEach(function() {
       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.onDataReceived(data);

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

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

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

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

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

@@ -269,3 +269,7 @@ $checkboxImageUrl: '../img/checkbox.png';
 $card-background: linear-gradient(135deg, #2f2f2f, #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);
+
+// footer
+$footer-link-color:   $gray-1;
+$footer-link-hover:   $gray-4;

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

@@ -293,3 +293,7 @@ $checkboxImageUrl: '../img/checkbox_white.png';
 $card-background: linear-gradient(135deg, $gray-5, $gray-6);
 $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);
+
+// 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 {
-  padding: 20px;
   display: table-cell;
   vertical-align: middle;
   text-align: center;

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

@@ -4,7 +4,7 @@
 }
 
 .main-view {
-  height: 100%;
+  // height: 100%; REMOVED FOR FOOTER TRW
 }
 
 .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;
 
       beforeEach(function() {
@@ -64,13 +64,13 @@ define([
       });
 
       it('missing datapoint with ms precision', function() {
-        fakedata.datapoints[0] = [1234567890000, 1337];
+        fakedata.datapoints[0] = [1337, 1234567890000];
         series = new TimeSeries(fakedata);
         expect(series.isMsResolutionNeeded()).to.be(false);
       });
 
       it('contains datapoint with ms precision', function() {
-        fakedata.datapoints[0] = [1236547890001, 1337];
+        fakedata.datapoints[0] = [1337, 1236547890001];
         series = new TimeSeries(fakedata);
         expect(series.isMsResolutionNeeded()).to.be(true);
       });

+ 6 - 0
public/test/core/utils/kbn_specs.js

@@ -147,5 +147,11 @@ define([
       var str = kbn.calculateInterval(range, 1000, '>10s');
       expect(str).to.be('20m');
     });
+	
+    it('10s 900 resolution and user low limit in ms', function() {
+      var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
+      var str = kbn.calculateInterval(range, 900, '>15ms');
+      expect(str).to.be('15ms');
+    });
   });
 });

+ 15 - 3
public/test/specs/dashboardViewStateSrv-specs.js

@@ -5,8 +5,20 @@ define([
 
   describe('when updating view state', function() {
     var viewState, location;
+    var timeSrv = {};
+    var templateSrv = {};
+    var contextSrv = {
+      user: {
+        orgId: 19
+      }
+    };
 
     beforeEach(module('grafana.services'));
+    beforeEach(module(function($provide) {
+      $provide.value('timeSrv', timeSrv);
+      $provide.value('templateSrv', templateSrv);
+      $provide.value('contextSrv', contextSrv);
+    }));
 
     beforeEach(inject(function(dashboardViewStateSrv, $location, $rootScope) {
       $rootScope.onAppEvent = function() {};
@@ -17,9 +29,9 @@ define([
 
     describe('to fullscreen true and edit true', function() {
       it('should update querystring and view state', function() {
-        var updateState = { fullscreen: true, edit: true, panelId: 1 };
+        var updateState = {fullscreen: true, edit: true, panelId: 1};
         viewState.update(updateState);
-        expect(location.search()).to.eql(updateState);
+        expect(location.search()).to.eql({fullscreen: true, edit: true, panelId: 1, org: 19});
         expect(viewState.dashboard.meta.fullscreen).to.be(true);
         expect(viewState.state.fullscreen).to.be(true);
       });
@@ -29,7 +41,7 @@ define([
       it('should remove params from query string', function() {
         viewState.update({fullscreen: true, panelId: 1, edit: true});
         viewState.update({fullscreen: false});
-        expect(location.search()).to.eql({});
+        expect(location.search()).to.eql({org: 19});
         expect(viewState.dashboard.meta.fullscreen).to.be(false);
         expect(viewState.state.fullscreen).to.be(null);
       });

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

@@ -141,8 +141,8 @@ define([
       });
 
       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);
       });
 
+      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() {

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

@@ -126,6 +126,80 @@ 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 multi select and $__all selected', function(scenario) {
+      scenario.setup(function() {
+        scenario.variable = {
+          type: 'query',
+          query: '',
+          name: 'test',
+          includeAll: true,
+          current: {
+            value: ['$__all'],
+            text: 'All'
+          }
+        };
+        scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
+      });
+
+      it('should keep current All value', function() {
+        expect(scenario.variable.current.value).to.eql(['$__all']);
+        expect(scenario.variable.current.text).to.eql('All');
+      });
+    });
+
+    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) {
       scenario.setup(function() {
         scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' };

+ 35 - 0
public/views/index.html

@@ -39,6 +39,41 @@
 			</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>
   </body>
 

+ 5 - 3
vendor/phantomjs/render.js

@@ -38,10 +38,11 @@
     function checkIsReady() {
       var canvas = page.evaluate(function() {
         if (!window.angular) { return false; }
-        var body = window.angular.element(document.body);   // 1
-        if (!body.scope) { return false; }
+        var body = window.angular.element(document.body);
+        if (!body.injector) { return false; }
+        if (!body.injector()) { return false; }
 
-        var rootScope = body.scope();
+        var rootScope = body.injector().get('$rootScope');
         if (!rootScope) {return false;}
         if (!rootScope.performance) { return false; }
         var panelsToLoad = window.angular.element('div.panel').length;
@@ -59,6 +60,7 @@
           width:  bb.width,
           height: bb.height
         };
+
         page.render(params.png);
         phantom.exit();
       }