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:
 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)
 
 

+ 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
 $ git clone https://github.com/grafana/docs-base
@@ -9,10 +17,45 @@ $ cd docs-base
 $ make docs-build
 $ 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
 $ 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.
 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/user.md', 'API', 'User API']
 - ['http_api/admin.md', 'API', 'Admin API']
 - ['http_api/admin.md', 'API', 'Admin API']
 - ['http_api/snapshot.md', 'API', 'Snapshot API']
 - ['http_api/snapshot.md', 'API', 'Snapshot API']
+- ['http_api/preferences.md', 'API', 'Preferences API']
 - ['http_api/other.md', 'API', 'Other API']
 - ['http_api/other.md', 'API', 'Other API']
 
 
 - ['plugins/index.md', 'Plugins', 'Overview']
 - ['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
 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"
 }
 }

+ 1 - 1
package.json

@@ -20,7 +20,7 @@
     "grunt-angular-templates": "^0.5.5",
     "grunt-angular-templates": "^0.5.5",
     "grunt-cli": "~0.1.13",
     "grunt-cli": "~0.1.13",
     "grunt-contrib-clean": "~0.7.0",
     "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-concat": "^0.5.1",
     "grunt-contrib-copy": "~0.8.2",
     "grunt-contrib-copy": "~0.8.2",
     "grunt-contrib-cssmin": "~0.14.0",
     "grunt-contrib-cssmin": "~0.14.0",

+ 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

+ 2 - 1
pkg/api/api.go

@@ -117,6 +117,7 @@ func Register(r *macaron.Macaron) {
 			r.Get("/:id", wrap(GetUserById))
 			r.Get("/:id", wrap(GetUserById))
 			r.Get("/:id/orgs", wrap(GetUserOrgList))
 			r.Get("/:id/orgs", wrap(GetUserOrgList))
 			r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
 			r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
+			r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
 		}, reqGrafanaAdmin)
 		}, reqGrafanaAdmin)
 
 
 		// org information available to all users.
 		// org information available to all users.
@@ -211,7 +212,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,
 	}
 	}

+ 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/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/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/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",
 		"AWS/ElasticMapReduce": {"IsIdle", "JobsRunning", "JobsFailed",
 			"MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen",
 			"MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen",
 			"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers",
 			"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers",
@@ -85,6 +94,7 @@ func init() {
 		"AWS/EBS":              {"VolumeId"},
 		"AWS/EBS":              {"VolumeId"},
 		"AWS/EC2":              {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
 		"AWS/EC2":              {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
 		"AWS/ELB":              {"LoadBalancerName", "AvailabilityZone"},
 		"AWS/ELB":              {"LoadBalancerName", "AvailabilityZone"},
+		"AWS/ElasticBeanstalk": {"EnvironmentName", "InstanceId"},
 		"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
 		"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
 		"AWS/ES":               {"ClientId", "DomainName"},
 		"AWS/ES":               {"ClientId", "DomainName"},
 		"AWS/Events":           {"RuleName"},
 		"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/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"
@@ -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
 	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{}
@@ -205,11 +203,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) {

+ 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))
 			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
 		// clear cookie headers
 		req.Header.Del("Cookie")
 		req.Header.Del("Cookie")
 		req.Header.Del("Set-Cookie")
 		req.Header.Del("Set-Cookie")

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

+ 18 - 0
pkg/api/user.go

@@ -40,6 +40,24 @@ func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
 	return handleUpdateUser(cmd)
 	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 {
 func handleUpdateUser(cmd m.UpdateUserCommand) Response {
 	if len(cmd.Login) == 0 {
 	if len(cmd.Login) == 0 {
 		cmd.Login = cmd.Email
 		cmd.Login = cmd.Email

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

@@ -25,7 +25,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();
+              });
             });
             });
           };
           };
 
 

+ 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);
         registerPluginComponent(scope, elem, attrs, componentInfo);
       }).catch(err => {
       }).catch(err => {
         $rootScope.appEvent('alert-error', ['Plugin Error', err.message || 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 requestIsLocal = options.url.indexOf('/') === 0;
       var firstAttempt = options.retry === 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) {
       return $http(options).then(null, function(err) {
         // handle unauthorized for backend requests
         // handle unauthorized for backend requests
         if (requestIsLocal && firstAttempt  && err.status === 401) {
         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() {
     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;
         }
         }

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

@@ -12,9 +12,21 @@ function($, _) {
 
 
   kbn.round_interval = function(interval) {
   kbn.round_interval = function(interval) {
     switch (true) {
     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
     // 0.75s
     case (interval <= 750):
     case (interval <= 750):
       return 500;       // 0.5s
       return 500;       // 0.5s
@@ -133,7 +145,7 @@ function($, _) {
     return str;
     return str;
   };
   };
 
 
-  kbn.interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/;
+  kbn.interval_regex = /(\d+(?:\.\d+)?)(ms|[Mwdhmsy])/;
 
 
   // histogram & trends
   // histogram & trends
   kbn.intervals_in_seconds = {
   kbn.intervals_in_seconds = {
@@ -143,7 +155,8 @@ function($, _) {
     d: 86400,
     d: 86400,
     h: 3600,
     h: 3600,
     m: 60,
     m: 60,
-    s: 1
+    s: 1,
+    ms: 0.001
   };
   };
 
 
   kbn.calculateInterval = function(range, resolution, userInterval) {
   kbn.calculateInterval = function(range, resolution, userInterval) {

+ 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

@@ -70,12 +70,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';
     };
     };

+ 6 - 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, private dynamicDashboardSrv) {
+  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;
   }
   }

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

@@ -8,7 +8,7 @@ function (angular, _, $) {
 
 
   var module = angular.module('grafana.services');
   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
     // represents the transient view state
     // like fullscreen panel & edit
     // 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() {
       $scope.onAppEvent('$routeUpdate', function() {
         var urlState = self.getQueryStringState();
         var urlState = self.getQueryStringState();
         if (self.needsSync(urlState)) {
         if (self.needsSync(urlState)) {
@@ -40,10 +53,26 @@ function (angular, _, $) {
         self.registerPanel(payload.scope);
         self.registerPanel(payload.scope);
       });
       });
 
 
-      this.update(this.getQueryStringState(), true);
+      this.update(this.getQueryStringState());
       this.expandRowForPanel();
       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() {
     DashboardViewState.prototype.expandRowForPanel = function() {
       if (!this.state.panelId) { return; }
       if (!this.state.panelId) { return; }
 
 
@@ -63,6 +92,7 @@ function (angular, _, $) {
       state.fullscreen = state.fullscreen ? true : null;
       state.fullscreen = state.fullscreen ? true : null;
       state.edit =  (state.edit === "true" || state.edit === true) || null;
       state.edit =  (state.edit === "true" || state.edit === true) || null;
       state.editview = state.editview || null;
       state.editview = state.editview || null;
+      state.org = contextSrv.user.orgId;
       return state;
       return state;
     };
     };
 
 
@@ -70,10 +100,11 @@ function (angular, _, $) {
       var urlState = _.clone(this.state);
       var urlState = _.clone(this.state);
       urlState.fullscreen = this.state.fullscreen ? true : null;
       urlState.fullscreen = this.state.fullscreen ? true : null;
       urlState.edit = this.state.edit ? true : null;
       urlState.edit = this.state.edit ? true : null;
+      urlState.org = contextSrv.user.orgId;
       return urlState;
       return urlState;
     };
     };
 
 
-    DashboardViewState.prototype.update = function(state, skipUrlSync) {
+    DashboardViewState.prototype.update = function(state) {
       _.extend(this.state, state);
       _.extend(this.state, state);
       this.dashboard.meta.fullscreen = this.state.fullscreen;
       this.dashboard.meta.fullscreen = this.state.fullscreen;
 
 
@@ -83,10 +114,7 @@ function (angular, _, $) {
         this.state.edit = null;
         this.state.edit = null;
       }
       }
 
 
-      if (!skipUrlSync) {
-        $location.search(this.serializeToUrl());
-      }
-
+      $location.search(this.serializeToUrl());
       this.syncState();
       this.syncState();
     };
     };
 
 

+ 2 - 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';
 
 
@@ -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?) {
   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>
+<div class="clearfix"></div>

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

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

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

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

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

@@ -152,6 +152,14 @@
 				</div>
 				</div>
 			</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">
 			<div ng-show="current.type === 'query'" class="gf-form-group">
         <h5 class="section-heading">Query Options</h5>
         <h5 class="section-heading">Query Options</h5>
 
 
@@ -214,7 +222,7 @@
         </div>
         </div>
       </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>
         <h5 class="section-heading">Selection Options</h5>
         <div class="section">
         <div class="section">
           <gf-form-switch class="gf-form"
           <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");
       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) {

+ 41 - 8
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);
@@ -125,8 +127,8 @@ function (angular, _, kbn) {
     this.setVariableValue = function(variable, option, initPhase) {
     this.setVariableValue = function(variable, option, initPhase) {
       variable.current = angular.copy(option);
       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);
       self.selectOptionsForCurrentValue(variable);
@@ -166,6 +168,11 @@ function (angular, _, kbn) {
         return;
         return;
       }
       }
 
 
+      if (variable.type === 'constant') {
+        variable.options = [{text: variable.query, value: variable.query}];
+        return;
+      }
+
       // extract options in comma seperated string
       // extract options in comma seperated string
       variable.options = _.map(variable.query.split(/[,]+/), function(text) {
       variable.options = _.map(variable.query.split(/[,]+/), function(text) {
         return { text: text.trim(), value: text.trim() };
         return { text: text.trim(), value: text.trim() };
@@ -173,6 +180,7 @@ function (angular, _, kbn) {
 
 
       if (variable.type === 'interval') {
       if (variable.type === 'interval') {
         self.updateAutoInterval(variable);
         self.updateAutoInterval(variable);
+        return;
       }
       }
 
 
       if (variable.type === 'custom' && variable.includeAll) {
       if (variable.type === 'custom' && variable.includeAll) {
@@ -224,6 +232,7 @@ function (angular, _, kbn) {
 
 
     this.selectOptionsForCurrentValue = function(variable) {
     this.selectOptionsForCurrentValue = function(variable) {
       var i, y, value, option;
       var i, y, value, option;
+      var selected = [];
 
 
       for (i = 0; i < variable.options.length; i++) {
       for (i = 0; i < variable.options.length; i++) {
         option = variable.options[i];
         option = variable.options[i];
@@ -233,28 +242,44 @@ function (angular, _, kbn) {
             value = variable.current.value[y];
             value = variable.current.value[y];
             if (option.value === value) {
             if (option.value === value) {
               option.selected = true;
               option.selected = true;
+              selected.push(option);
             }
             }
           }
           }
         } else if (option.value === variable.current.value) {
         } else if (option.value === variable.current.value) {
           option.selected = true;
           option.selected = true;
+          selected.push(option);
         }
         }
       }
       }
+
+      return selected;
     };
     };
 
 
     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);
+        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 {
       } 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 $q.when(null); }
           return self.setVariableValue(variable, variable.options[0]);
           return self.setVariableValue(variable, variable.options[0]);
         }
         }
       }
       }
@@ -313,6 +338,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

@@ -269,3 +269,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

@@ -293,3 +293,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);
       });
       });

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

@@ -147,5 +147,11 @@ define([
       var str = kbn.calculateInterval(range, 1000, '>10s');
       var str = kbn.calculateInterval(range, 1000, '>10s');
       expect(str).to.be('20m');
       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() {
   describe('when updating view state', function() {
     var viewState, location;
     var viewState, location;
+    var timeSrv = {};
+    var templateSrv = {};
+    var contextSrv = {
+      user: {
+        orgId: 19
+      }
+    };
 
 
     beforeEach(module('grafana.services'));
     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) {
     beforeEach(inject(function(dashboardViewStateSrv, $location, $rootScope) {
       $rootScope.onAppEvent = function() {};
       $rootScope.onAppEvent = function() {};
@@ -17,9 +29,9 @@ define([
 
 
     describe('to fullscreen true and edit true', function() {
     describe('to fullscreen true and edit true', function() {
       it('should update querystring and view state', 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);
         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.dashboard.meta.fullscreen).to.be(true);
         expect(viewState.state.fullscreen).to.be(true);
         expect(viewState.state.fullscreen).to.be(true);
       });
       });
@@ -29,7 +41,7 @@ define([
       it('should remove params from query string', function() {
       it('should remove params from query string', function() {
         viewState.update({fullscreen: true, panelId: 1, edit: true});
         viewState.update({fullscreen: true, panelId: 1, edit: true});
         viewState.update({fullscreen: false});
         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.dashboard.meta.fullscreen).to.be(false);
         expect(viewState.state.fullscreen).to.be(null);
         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() {
       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() {

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

+ 5 - 3
vendor/phantomjs/render.js

@@ -38,10 +38,11 @@
     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
-        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) {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;
@@ -59,6 +60,7 @@
           width:  bb.width,
           width:  bb.width,
           height: bb.height
           height: bb.height
         };
         };
+
         page.render(params.png);
         page.render(params.png);
         phantom.exit();
         phantom.exit();
       }
       }