Selaa lähdekoodia

Merge branch 'master' into query-editor-style

Torkel Ödegaard 9 vuotta sitten
vanhempi
commit
68bd82f1b6
100 muutettua tiedostoa jossa 1201 lisäystä ja 577 poistoa
  1. 32 3
      CHANGELOG.md
  2. 4 1
      README.md
  3. 7 0
      conf/defaults.ini
  4. 7 0
      conf/sample.ini
  5. 0 29
      docs/sources/http_api/data_source.md
  6. 3 3
      docs/sources/installation/debian.md
  7. 4 4
      docs/sources/installation/rpm.md
  8. 1 1
      docs/sources/installation/windows.md
  9. 2 2
      docs/sources/plugins/development.md
  10. 1 1
      docs/sources/plugins/index.md
  11. 4 4
      docs/sources/plugins/installation.md
  12. 1 1
      docs/sources/plugins/panels.md
  13. 3 2
      karma.conf.js
  14. 2 1
      latest.json
  15. 3 3
      package.json
  16. 9 9
      packaging/publish/publish.sh
  17. 7 3
      pkg/api/api.go
  18. 1 0
      pkg/api/avatar/avatar.go
  19. 11 6
      pkg/api/dtos/plugins.go
  20. 5 3
      pkg/api/frontendsettings.go
  21. 46 0
      pkg/api/gnetproxy.go
  22. 14 4
      pkg/api/plugins.go
  23. 18 0
      pkg/api/user.go
  24. 16 9
      pkg/cmd/grafana-cli/commands/commands.go
  25. 12 7
      pkg/cmd/grafana-cli/commands/install_command.go
  26. 1 1
      pkg/cmd/grafana-cli/commands/upgrade_all_command.go
  27. 24 4
      pkg/cmd/grafana-cli/main.go
  28. 1 1
      pkg/cmd/grafana-server/main.go
  29. 4 0
      pkg/metrics/report_usage.go
  30. 8 9
      pkg/models/stats.go
  31. 4 3
      pkg/plugins/frontend_plugin.go
  32. 4 0
      pkg/plugins/models.go
  33. 4 0
      pkg/plugins/plugins.go
  34. 119 0
      pkg/plugins/update_checker.go
  35. 8 6
      pkg/services/sqlstore/migrations/preferences_mig.go
  36. 1 6
      pkg/services/sqlstore/stats.go
  37. 2 0
      pkg/setting/setting.go
  38. 2 11
      public/app/core/components/colorpicker.ts
  39. 3 0
      public/app/core/components/dashboard_selector.ts
  40. 16 7
      public/app/core/components/info_popover.ts
  41. 0 4
      public/app/core/components/sidemenu/sidemenu.html
  42. 2 9
      public/app/core/components/sidemenu/sidemenu.ts
  43. 9 23
      public/app/core/components/switch.ts
  44. 3 1
      public/app/core/controllers/login_ctrl.js
  45. 1 1
      public/app/core/directives/plugin_component.ts
  46. 4 1
      public/app/core/services/popover_srv.ts
  47. 3 1
      public/app/core/time_series2.ts
  48. 8 29
      public/app/core/utils/emitter.ts
  49. 0 4
      public/app/features/admin/partials/stats.html
  50. 1 0
      public/app/features/annotations/editor_ctrl.js
  51. 2 0
      public/app/features/dashboard/dashboardSrv.js
  52. 1 2
      public/app/features/dashboard/dashnav/dashnav.html
  53. 8 0
      public/app/features/dashboard/keybindings.js
  54. 7 4
      public/app/features/dashboard/partials/settings.html
  55. 24 28
      public/app/features/dashboard/partials/shareModal.html
  56. 8 4
      public/app/features/dashboard/timepicker/timepicker.html
  57. 26 0
      public/app/features/dashboard/timepicker/timepicker.ts
  58. 0 1
      public/app/features/dashboard/unsavedChangesSrv.js
  59. 3 4
      public/app/features/org/prefs_control.ts
  60. 8 4
      public/app/features/panel/metrics_panel_ctrl.ts
  61. 1 0
      public/app/features/panel/panel_ctrl.ts
  62. 1 1
      public/app/features/panel/partials/panelTime.html
  63. 10 3
      public/app/features/plugins/ds_edit_ctrl.ts
  64. 8 12
      public/app/features/plugins/partials/ds_edit.html
  65. 30 25
      public/app/features/plugins/partials/ds_http_settings.html
  66. 3 2
      public/app/features/plugins/partials/ds_list.html
  67. 5 2
      public/app/features/plugins/partials/plugin_edit.html
  68. 7 2
      public/app/features/plugins/partials/plugin_list.html
  69. 21 0
      public/app/features/plugins/partials/update_instructions.html
  70. 13 2
      public/app/features/plugins/plugin_edit_ctrl.ts
  71. 51 50
      public/app/features/templating/partials/editor.html
  72. 6 4
      public/app/features/templating/templateValuesSrv.js
  73. 6 2
      public/app/headers/common.d.ts
  74. 0 73
      public/app/headers/es6-promise/es6-promise.d.ts
  75. 5 3
      public/app/headers/es6-shim/es6-shim.d.ts
  76. 16 8
      public/app/partials/help_modal.html
  77. 3 3
      public/app/partials/login.html
  78. 2 1
      public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html
  79. 17 12
      public/app/plugins/datasource/cloudwatch/partials/config.html
  80. 14 2
      public/app/plugins/datasource/influxdb/datasource.ts
  81. 1 1
      public/app/plugins/datasource/opentsdb/config_ctrl.ts
  82. 1 1
      public/app/plugins/datasource/prometheus/datasource.ts
  83. 25 33
      public/app/plugins/panel/dashlist/editor.html
  84. 16 11
      public/app/plugins/panel/dashlist/module.html
  85. 79 25
      public/app/plugins/panel/dashlist/module.ts
  86. 4 2
      public/app/plugins/panel/graph/graph.js
  87. 12 10
      public/app/plugins/panel/graph/graph_tooltip.js
  88. 2 3
      public/app/plugins/panel/graph/legend.js
  89. 18 5
      public/app/plugins/panel/graph/module.ts
  90. 4 0
      public/app/plugins/panel/graph/series_overrides_ctrl.js
  91. 1 1
      public/app/plugins/panel/graph/tab_display.html
  92. 1 2
      public/app/plugins/panel/graph/tab_legend.html
  93. 2 0
      public/app/plugins/panel/pluginlist/README.md
  94. 40 0
      public/app/plugins/panel/pluginlist/editor.html
  95. 119 0
      public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg
  96. 30 0
      public/app/plugins/panel/pluginlist/module.html
  97. 74 0
      public/app/plugins/panel/pluginlist/module.ts
  98. 16 0
      public/app/plugins/panel/pluginlist/plugin.json
  99. 1 5
      public/app/plugins/panel/singlestat/module.ts
  100. 4 7
      public/app/plugins/panel/table/module.ts

+ 32 - 3
CHANGELOG.md

@@ -1,4 +1,34 @@
-# 3.0.0-beta2 (unreleased)
+# 3.0.0-beta5 (2016-04-15)
+
+### Bug fixes
+* **grafana-cli**: Fixed issue grafana-cli tool, did not detect the right plugin dir, fixes [#4723](https://github.com/grafana/grafana/issues/4723)
+* **Graph**: Fixed issue with light theme text color issue in tooltip, fixes [#4702](https://github.com/grafana/grafana/issues/4702)
+* **Snapshot**: Fixed issue with empty snapshots, fixes [#4706](https://github.com/grafana/grafana/issues/4706)
+
+# 3.0.0-beta4 (2016-04-13)
+
+### Bug fixes
+* **Home dashboard**: Fixed issue with permission denied error on home dashboard, fixes [#4686](https://github.com/grafana/grafana/issues/4686)
+* **Templating**: Fixed issue templating variables that use regex extraction, fixes [#4672](https://github.com/grafana/grafana/issues/4672)
+
+# 3.0.0-beta3 (2016-04-12)
+
+### Enhancements
+* **InfluxDB**: Changed multi query encoding to work with InfluxDB 0.11 & 0.12, closes [#4533](https://github.com/grafana/grafana/issues/4533)
+* **Timepicker**: Add arrows and shortcuts for moving back and forth in current dashboard, closes [#119](https://github.com/grafana/grafana/issues/119)
+
+### Bug fixes
+* **Postgres**: Fixed page render crash when using postgres, fixes [#4558](https://github.com/grafana/grafana/issues/4558)
+* **Table panel**: Fixed table panel bug when trying to show annotations in table panel, fixes [#4563](https://github.com/grafana/grafana/issues/4563)
+* **App Config**: Fixed app config issue showing content of other app config, fixes [#4575](https://github.com/grafana/grafana/issues/4575)
+* **Graph Panel**: Fixed legend option max not updating, fixes [#4601](https://github.com/grafana/grafana/issues/4601)
+* **Graph Panel**: Fixed issue where newly added graph panels shared same axes config, fixes [#4582](https://github.com/grafana/grafana/issues/4582)
+* **Graph Panel**: Fixed issue with axis labels overlapping Y-axis, fixes [#4626](https://github.com/grafana/grafana/issues/4626)
+* **InfluxDB**: Fixed issue with templating query containing template variable, fixes [#4602](https://github.com/grafana/grafana/issues/4602)
+* **Graph Panel**: Fixed issue with hiding series and stacking, fixes [#4557](https://github.com/grafana/grafana/issues/4557)
+* **Graph Panel**: Fixed issue with legend height in table mode with few series, affected iframe embedding as well, fixes [#4640](https://github.com/grafana/grafana/issues/4640)
+
+# 3.0.0-beta2 (2016-04-04)
 
 ### New Features (introduces since 3.0-beta1)
 * **Preferences**: Set home dashboard on user and org level, closes [#1678](https://github.com/grafana/grafana/issues/1678)
@@ -9,9 +39,8 @@
 * **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529)
 * **Table Panel**: Fixed issue with table panel sort, fixes [#4532](https://github.com/grafana/grafana/issues/4532)
 * **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536)
-* **Metrics tab**: Fix for missing datasource name in datasource selector, fixes [#4540](https://github.com/grafana/grafana/issues/4540)
+* **Metrics tab**: Fix for missing datasource name in datasource selector, fixes [#4541](https://github.com/grafana/grafana/issues/4540)
 * **Graph**: Fix legend in table mode with series on right-y axis, fixes [#4551](https://github.com/grafana/grafana/issues/4551), [#1145](https://github.com/grafana/grafana/issues/1145)
-* **Password**: Password reset link/page did not work, fixes [#4542](https://github.com/grafana/grafana/issues/4542)
 
 # 3.0.0-beta1 (2016-03-31)
 

+ 4 - 1
README.md

@@ -1,8 +1,10 @@
-[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/grafana/grafana?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana)
 ================
 [Website](http://grafana.org) |
 [Twitter](https://twitter.com/grafana) |
 [IRC](https://webchat.freenode.net/?channels=grafana) |
+![](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com)
+[Slack](http://slack.raintank.io) |
 [Email](mailto:contact@grafana.org)
 
 Grafana is an open source, feature rich metrics dashboard and graph editor for
@@ -77,6 +79,7 @@ the latest master builds [here](http://grafana.org/download/builds)
 
 - Go 1.5
 - NodeJS
+- [Godep](https://github.com/tools/godep)
 
 ### Get Code
 

+ 7 - 0
conf/defaults.ini

@@ -111,6 +111,13 @@ gc_interval_time = 86400
 # Change this option to false to disable reporting.
 reporting_enabled = true
 
+# Set to false to disable all checks to https://grafana.net
+# for new vesions (grafana itself and plugins), check is used
+# in some UI views to notify that grafana or plugin update exists
+# This option does not cause any auto updates, nor send any information
+# only a GET request to http://grafana.net to get latest versions
+check_for_updates = true
+
 # Google Analytics universal tracking code, only enabled if you specify an id here
 google_analytics_ua_id =
 

+ 7 - 0
conf/sample.ini

@@ -100,6 +100,13 @@
 # Change this option to false to disable reporting.
 ;reporting_enabled = true
 
+# Set to false to disable all checks to https://grafana.net
+# for new vesions (grafana itself and plugins), check is used
+# in some UI views to notify that grafana or plugin update exists
+# This option does not cause any auto updates, nor send any information
+# only a GET request to http://grafana.net to get latest versions
+check_for_updates = true
+
 # Google Analytics universal tracking code, only enabled if you specify an id here
 ;google_analytics_ua_id =
 

+ 0 - 29
docs/sources/http_api/data_source.md

@@ -207,35 +207,6 @@ page_keywords: grafana, admin, http, api, documentation, datasource
 
     {"message":"Data source deleted"}
 
-## Available data source types
-
-`GET /api/datasources/plugins`
-
-**Example Request**:
-
-    GET /api/datasources/plugins HTTP/1.1
-    Accept: application/json
-    Content-Type: application/json
-    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
-
-**Example Response**:
-
-    HTTP/1.1 200
-    Content-Type: application/json
-
-    {
-      "grafana":{
-        "metrics":true,"module":"plugins/datasource/grafana/datasource",
-        "name":"Grafana (for testing)",
-        "partials":{
-          "query":"app/plugins/datasource/grafana/partials/query.editor.html"
-        },
-        "pluginType":"datasource",
-        "serviceName":"GrafanaDatasource",
-        "type":"grafana"
-      }
-    }
-
 ## Data source proxy calls
 
 `GET /api/datasources/proxy/:datasourceId/*`

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

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

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

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

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

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

+ 2 - 2
docs/sources/plugins/development.md

@@ -10,7 +10,7 @@ From grafana 3.0 it's very easy to develop your own plugins and share them with
 
 ## Short version
 
-1. [Setup grafana](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md)
+1. [Setup grafana](http://docs.grafana.org/project/building_from_source/)
 2. Clone an example plugin into ```/var/lib/grafana/plugins```  or `data/plugins` (relative to grafana git repo if your running development version from source dir)
 3. Code away!
 
@@ -34,7 +34,7 @@ and [apps](./apps.md) plugins in the documentation.
 ## Start developing your plugin
 There are two ways that you can start developing a Grafana plugin.
 
-1. Setup a Grafana development environment. [(described here)](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md) and place your plugin in the ```data/plugins``` folder.
+1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder.
 2. Install Grafana and place your plugin in the plugins directory which is set in your [config file](../installation/configuration.md). By default this is `/var/lib/grafana/plugins` on Linux systems.
 3. Place your plugin directory anywhere you like and specify it grafana.ini.
 

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

@@ -9,7 +9,7 @@ page_keywords: grafana, plugins, documentation
 From Grafana 3.0 not only datasource plugins are supported but also panel plugins and apps.
 Having panels as plugins make it easy to create and add any kind of panel, to show your data
 or improve your favorite dashboards. Apps is something new in Grafana that enables
-bundling of datasources, panels, dashboards and Grafana pages into a cohesive experiance.
+bundling of datasources, panels, dashboards and Grafana pages into a cohesive experience.
 
 Grafana already have a strong community of contributors and plugin developers.
 By making it easier to develop and install plugins we hope that the community

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

@@ -28,14 +28,14 @@ List installed plugins
 grafana-cli plugins ls
 ```
 
-Upgrade all installed plugins
+Update all installed plugins
 ```
-grafana-cli plugins upgrade-all
+grafana-cli plugins update-all
 ```
 
-Upgrade one plugin
+Update one plugin
 ```
-grafana-cli plugins upgrade <plugin-id>
+grafana-cli plugins update <plugin-id>
 ```
 
 Remove one plugin

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

@@ -7,7 +7,7 @@ page_keywords: grafana, plugins, documentation
 
 # Panels
 
-Panels are the main bulding block of dashboards.
+Panels are the main building blocks of dashboards.
 
 ## Panel development
 

+ 3 - 2
karma.conf.js

@@ -24,9 +24,10 @@ module.exports = function(config) {
     logLevel: config.LOG_INFO,
     autoWatch: true,
     browsers: ['PhantomJS'],
-    captureTimeout: 2000,
+    captureTimeout: 20000,
     singleRun: true,
-    autoWatchBatchDelay: 1000,
+    autoWatchBatchDelay: 10000,
+    browserNoActivityTimeout: 60000,
 
   });
 

+ 2 - 1
latest.json

@@ -1,3 +1,4 @@
 {
-	"version": "2.1.1"
+  "stable": "2.6.0",
+	"testing": "3.0.0-beta5"
 }

+ 3 - 3
package.json

@@ -4,13 +4,12 @@
     "company": "Coding Instinct AB"
   },
   "name": "grafana",
-  "version": "3.0.0-beta2",
+  "version": "3.0.0-beta6",
   "repository": {
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
   },
   "devDependencies": {
-    "angular2": "2.0.0-beta.12",
     "zone.js": "^0.6.6",
     "autoprefixer": "^6.3.3",
     "es6-promise": "^3.0.2",
@@ -54,7 +53,7 @@
     "mocha": "2.3.4",
     "phantomjs-prebuilt": "^2.1.3",
     "reflect-metadata": "0.1.2",
-    "rxjs": "5.0.0-beta.2",
+    "rxjs": "5.0.0-beta.4",
     "sass-lint": "^1.5.0",
     "systemjs": "0.19.24"
   },
@@ -68,6 +67,7 @@
   },
   "license": "Apache-2.0",
   "dependencies": {
+    "eventemitter3": "^1.2.0",
     "grunt-jscs": "~1.5.x",
     "grunt-sass-lint": "^0.1.0",
     "grunt-sync": "^0.4.1",

+ 9 - 9
packaging/publish/publish.sh

@@ -1,21 +1,21 @@
 #! /usr/bin/env bash
 
-deb_ver=3.0.0-beta11459429091
-rpm_ver=3.0.0-beta11459429091
+deb_ver=3.0.0-beta51460725904
+rpm_ver=3.0.0-beta51460725904
 
 #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/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-${version}-1.x86_64.rpm

+ 7 - 3
pkg/api/api.go

@@ -30,6 +30,7 @@ func Register(r *macaron.Macaron) {
 	// authed views
 	r.Get("/profile/", reqSignedIn, Index)
 	r.Get("/profile/password", reqSignedIn, Index)
+	r.Get("/profile/switch-org/:id", reqSignedIn, ChangeActiveOrgAndRedirectToHome)
 	r.Get("/org/", reqSignedIn, Index)
 	r.Get("/org/new", reqSignedIn, Index)
 	r.Get("/datasources/", reqSignedIn, Index)
@@ -190,12 +191,12 @@ func Register(r *macaron.Macaron) {
 
 		r.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn)
 
-		r.Group("/plugins", func() {
-			r.Get("/", wrap(GetPluginList))
+		r.Get("/plugins", wrap(GetPluginList))
+		r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
 
+		r.Group("/plugins", func() {
 			r.Get("/:pluginId/readme", wrap(GetPluginReadme))
 			r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
-			r.Get("/:pluginId/settings", wrap(GetPluginSettingById))
 			r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
 		}, reqOrgAdmin)
 
@@ -252,6 +253,9 @@ func Register(r *macaron.Macaron) {
 	// rendering
 	r.Get("/render/*", reqSignedIn, RenderToPng)
 
+	// grafana.net proxy
+	r.Any("/api/gnet/*", reqSignedIn, ProxyGnetRequest)
+
 	// Gravatar service.
 	avt := avatar.CacheServer()
 	r.Get("/avatar/:hash", avt.ServeHTTP)

+ 1 - 0
pkg/api/avatar/avatar.go

@@ -116,6 +116,7 @@ func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if avatar.Expired() {
 		if err := avatar.Update(); err != nil {
 			log.Trace("avatar update error: %v", err)
+			avatar = this.notFound
 		}
 	}
 

+ 11 - 6
pkg/api/dtos/plugins.go

@@ -15,15 +15,20 @@ type PluginSetting struct {
 	Dependencies  *plugins.PluginDependencies `json:"dependencies"`
 	JsonData      map[string]interface{}      `json:"jsonData"`
 	DefaultNavUrl string                      `json:"defaultNavUrl"`
+
+	LatestVersion string `json:"latestVersion"`
+	HasUpdate     bool   `json:"hasUpdate"`
 }
 
 type PluginListItem struct {
-	Name    string              `json:"name"`
-	Type    string              `json:"type"`
-	Id      string              `json:"id"`
-	Enabled bool                `json:"enabled"`
-	Pinned  bool                `json:"pinned"`
-	Info    *plugins.PluginInfo `json:"info"`
+	Name          string              `json:"name"`
+	Type          string              `json:"type"`
+	Id            string              `json:"id"`
+	Enabled       bool                `json:"enabled"`
+	Pinned        bool                `json:"pinned"`
+	Info          *plugins.PluginInfo `json:"info"`
+	LatestVersion string              `json:"latestVersion"`
+	HasUpdate     bool                `json:"hasUpdate"`
 }
 
 type PluginList []PluginListItem

+ 5 - 3
pkg/api/frontendsettings.go

@@ -137,9 +137,11 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 		"allowOrgCreate":    (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
 		"authProxyEnabled":  setting.AuthProxyEnabled,
 		"buildInfo": map[string]interface{}{
-			"version":    setting.BuildVersion,
-			"commit":     setting.BuildCommit,
-			"buildstamp": setting.BuildStamp,
+			"version":       setting.BuildVersion,
+			"commit":        setting.BuildCommit,
+			"buildstamp":    setting.BuildStamp,
+			"latestVersion": plugins.GrafanaLatestVersion,
+			"hasUpdate":     plugins.GrafanaHasUpdate,
 		},
 	}
 

+ 46 - 0
pkg/api/gnetproxy.go

@@ -0,0 +1,46 @@
+package api
+
+import (
+	"crypto/tls"
+	"net"
+	"net/http"
+	"net/http/httputil"
+	"time"
+
+	"github.com/grafana/grafana/pkg/middleware"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+var gNetProxyTransport = &http.Transport{
+	TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
+	Proxy:           http.ProxyFromEnvironment,
+	Dial: (&net.Dialer{
+		Timeout:   30 * time.Second,
+		KeepAlive: 30 * time.Second,
+	}).Dial,
+	TLSHandshakeTimeout: 10 * time.Second,
+}
+
+func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
+	director := func(req *http.Request) {
+		req.URL.Scheme = "https"
+		req.URL.Host = "grafana.net"
+		req.Host = "grafana.net"
+
+		req.URL.Path = util.JoinUrlFragments("https://grafana.net/api", proxyPath)
+
+		// clear cookie headers
+		req.Header.Del("Cookie")
+		req.Header.Del("Set-Cookie")
+	}
+
+	return &httputil.ReverseProxy{Director: director}
+}
+
+func ProxyGnetRequest(c *middleware.Context) {
+	proxyPath := c.Params("*")
+	proxy := ReverseProxyGnetReq(proxyPath)
+	proxy.Transport = gNetProxyTransport
+	proxy.ServeHTTP(c.Resp, c.Req.Request)
+	c.Resp.Header().Del("Set-Cookie")
+}

+ 14 - 4
pkg/api/plugins.go

@@ -14,6 +14,7 @@ func GetPluginList(c *middleware.Context) Response {
 	typeFilter := c.Query("type")
 	enabledFilter := c.Query("enabled")
 	embeddedFilter := c.Query("embedded")
+	coreFilter := c.Query("core")
 
 	pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
 
@@ -28,16 +29,23 @@ func GetPluginList(c *middleware.Context) Response {
 			continue
 		}
 
+		// filter out core plugins
+		if coreFilter == "0" && pluginDef.IsCorePlugin {
+			continue
+		}
+
 		// filter on type
 		if typeFilter != "" && typeFilter != pluginDef.Type {
 			continue
 		}
 
 		listItem := dtos.PluginListItem{
-			Id:   pluginDef.Id,
-			Name: pluginDef.Name,
-			Type: pluginDef.Type,
-			Info: &pluginDef.Info,
+			Id:            pluginDef.Id,
+			Name:          pluginDef.Name,
+			Type:          pluginDef.Type,
+			Info:          &pluginDef.Info,
+			LatestVersion: pluginDef.GrafanaNetVersion,
+			HasUpdate:     pluginDef.GrafanaNetHasUpdate,
 		}
 
 		if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
@@ -81,6 +89,8 @@ func GetPluginSettingById(c *middleware.Context) Response {
 			BaseUrl:       def.BaseUrl,
 			Module:        def.Module,
 			DefaultNavUrl: def.DefaultNavUrl,
+			LatestVersion: def.GrafanaNetVersion,
+			HasUpdate:     def.GrafanaNetHasUpdate,
 		}
 
 		query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId}

+ 18 - 0
pkg/api/user.go

@@ -4,6 +4,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 )
 
@@ -109,6 +110,23 @@ func UserSetUsingOrg(c *middleware.Context) Response {
 	return ApiSuccess("Active organization changed")
 }
 
+// GET /profile/switch-org/:id
+func ChangeActiveOrgAndRedirectToHome(c *middleware.Context) {
+	orgId := c.ParamsInt64(":id")
+
+	if !validateUsingOrg(c.UserId, orgId) {
+		NotFoundHandler(c)
+	}
+
+	cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		NotFoundHandler(c)
+	}
+
+	c.Redirect(setting.AppSubUrl + "/")
+}
+
 func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response {
 	userQuery := m.GetUserByIdQuery{Id: c.UserId}
 

+ 16 - 9
pkg/cmd/grafana-cli/commands/commands.go

@@ -1,9 +1,10 @@
 package commands
 
 import (
+	"os"
+
 	"github.com/codegangsta/cli"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
-	"os"
 )
 
 func runCommand(command func(commandLine CommandLine) error) func(context *cli.Context) {
@@ -25,27 +26,33 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C
 var pluginCommands = []cli.Command{
 	{
 		Name:   "install",
-		Usage:  "install <plugin name>",
+		Usage:  "install <plugin id>",
 		Action: runCommand(installCommand),
 	}, {
 		Name:   "list-remote",
 		Usage:  "list remote available plugins",
 		Action: runCommand(listremoteCommand),
 	}, {
-		Name:   "upgrade",
-		Usage:  "upgrade <plugin name>",
-		Action: runCommand(upgradeCommand),
+		Name:    "update",
+		Usage:   "update <plugin id>",
+		Aliases: []string{"upgrade"},
+		Action:  runCommand(upgradeCommand),
 	}, {
-		Name:   "upgrade-all",
-		Usage:  "upgrades all your installed plugins",
-		Action: runCommand(upgradeAllCommand),
+		Name:    "update-all",
+		Aliases: []string{"upgrade-all"},
+		Usage:   "update all your installed plugins",
+		Action:  runCommand(upgradeAllCommand),
 	}, {
 		Name:   "ls",
 		Usage:  "list all installed plugins",
 		Action: runCommand(lsCommand),
+	}, {
+		Name:   "uninstall",
+		Usage:  "uninstall <plugin id>",
+		Action: runCommand(removeCommand),
 	}, {
 		Name:   "remove",
-		Usage:  "remove <plugin name>",
+		Usage:  "remove <plugin id>",
 		Action: runCommand(removeCommand),
 	},
 }

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

@@ -120,6 +120,7 @@ func RemoveGitBuildFromName(pluginName, filename string) string {
 }
 
 var retryCount = 0
+var permissionsDeniedMessage = "Could not create %s. Permission denied. Make sure you have write access to plugindir"
 
 func downloadFile(pluginName, filePath, url string) (err error) {
 	defer func() {
@@ -153,16 +154,16 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 		newFile := path.Join(filePath, RemoveGitBuildFromName(pluginName, zf.Name))
 
 		if zf.FileInfo().IsDir() {
-			os.Mkdir(newFile, 0777)
+			err := os.Mkdir(newFile, 0777)
+			if PermissionsError(err) {
+				return fmt.Errorf(permissionsDeniedMessage, newFile)
+			}
 		} else {
 			dst, err := os.Create(newFile)
-			if err != nil {
-				if strings.Contains(err.Error(), "permission denied") {
-					return fmt.Errorf(
-						"Could not create file %s. permission deined. Make sure you have write access to plugindir",
-						newFile)
-				}
+			if PermissionsError(err) {
+				return fmt.Errorf(permissionsDeniedMessage, newFile)
 			}
+
 			defer dst.Close()
 			src, err := zf.Open()
 			if err != nil {
@@ -176,3 +177,7 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 
 	return nil
 }
+
+func PermissionsError(err error) bool {
+	return err != nil && strings.Contains(err.Error(), "permission denied")
+}

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

@@ -51,7 +51,7 @@ func upgradeAllCommand(c CommandLine) error {
 	}
 
 	for _, p := range pluginsToUpgrade {
-		log.Infof("Upgrading %v \n", p.Id)
+		log.Infof("Updating %v \n", p.Id)
 
 		s.RemoveInstalledPlugin(pluginsDir, p.Id)
 		InstallPlugin(p.Id, "", c)

+ 24 - 4
pkg/cmd/grafana-cli/main.go

@@ -8,17 +8,37 @@ import (
 	"github.com/codegangsta/cli"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
+	"strings"
 )
 
 var version = "master"
 
 func getGrafanaPluginDir() string {
-	os := runtime.GOOS
-	if os == "windows" {
+	currentOS := runtime.GOOS
+	defaultNix := "/var/lib/grafana/plugins"
+
+	if currentOS == "windows" {
 		return "C:\\opt\\grafana\\plugins"
-	} else {
-		return "/var/lib/grafana/plugins"
 	}
+
+	pwd, err := os.Getwd()
+
+	if err != nil {
+		log.Error("Could not get current path. using default")
+		return defaultNix
+	}
+
+	if isDevenvironment(pwd) {
+		return "../../../data/plugins"
+	}
+
+	return defaultNix
+}
+
+func isDevenvironment(pwd string) bool {
+	// if grafana-cli is executed from the cmd folder we can assume
+	// that its in development environment.
+	return strings.HasSuffix(pwd, "/pkg/cmd/grafana-cli")
 }
 
 func main() {

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

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

+ 4 - 0
pkg/metrics/report_usage.go

@@ -10,6 +10,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -56,6 +57,9 @@ func sendUsageStats() {
 	metrics["stats.users.count"] = statsQuery.Result.UserCount
 	metrics["stats.orgs.count"] = statsQuery.Result.OrgCount
 	metrics["stats.playlist.count"] = statsQuery.Result.PlaylistCount
+	metrics["stats.plugins.apps.count"] = len(plugins.Apps)
+	metrics["stats.plugins.panels.count"] = len(plugins.Panels)
+	metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
 
 	dsStats := m.GetDataSourceStatsQuery{}
 	if err := bus.Dispatch(&dsStats); err != nil {

+ 8 - 9
pkg/models/stats.go

@@ -21,15 +21,14 @@ type GetDataSourceStatsQuery struct {
 }
 
 type AdminStats struct {
-	UserCount         int `json:"user_count"`
-	OrgCount          int `json:"org_count"`
-	DashboardCount    int `json:"dashboard_count"`
-	DbSnapshotCount   int `json:"db_snapshot_count"`
-	DbTagCount        int `json:"db_tag_count"`
-	DataSourceCount   int `json:"data_source_count"`
-	PlaylistCount     int `json:"playlist_count"`
-	StarredDbCount    int `json:"starred_db_count"`
-	GrafanaAdminCount int `json:"grafana_admin_count"`
+	UserCount       int `json:"user_count"`
+	OrgCount        int `json:"org_count"`
+	DashboardCount  int `json:"dashboard_count"`
+	DbSnapshotCount int `json:"db_snapshot_count"`
+	DbTagCount      int `json:"db_tag_count"`
+	DataSourceCount int `json:"data_source_count"`
+	PlaylistCount   int `json:"playlist_count"`
+	StarredDbCount  int `json:"starred_db_count"`
 }
 
 type GetAdminStatsQuery struct {

+ 4 - 3
pkg/plugins/frontend_plugin.go

@@ -14,7 +14,7 @@ type FrontendPluginBase struct {
 }
 
 func (fp *FrontendPluginBase) initFrontendPlugin() {
-	if isInternalPlugin(fp.PluginDir) {
+	if isExternalPlugin(fp.PluginDir) {
 		StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
 			Directory: fp.PluginDir,
 			PluginId:  fp.Id,
@@ -48,17 +48,18 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
 
 func (fp *FrontendPluginBase) handleModuleDefaults() {
 
-	if isInternalPlugin(fp.PluginDir) {
+	if isExternalPlugin(fp.PluginDir) {
 		fp.Module = path.Join("plugins", fp.Id, "module")
 		fp.BaseUrl = path.Join("public/plugins", fp.Id)
 		return
 	}
 
+	fp.IsCorePlugin = true
 	fp.Module = path.Join("app/plugins", fp.Type, fp.Id, "module")
 	fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id)
 }
 
-func isInternalPlugin(pluginDir string) bool {
+func isExternalPlugin(pluginDir string) bool {
 	return !strings.Contains(pluginDir, setting.StaticRootPath)
 }
 

+ 4 - 0
pkg/plugins/models.go

@@ -43,6 +43,10 @@ type PluginBase struct {
 	IncludedInAppId string `json:"-"`
 	PluginDir       string `json:"-"`
 	DefaultNavUrl   string `json:"-"`
+	IsCorePlugin    bool   `json:"-"`
+
+	GrafanaNetVersion   string `json:"-"`
+	GrafanaNetHasUpdate bool   `json:"-"`
 
 	// cache for readme file contents
 	Readme []byte `json:"-"`

+ 4 - 0
pkg/plugins/plugins.go

@@ -22,6 +22,9 @@ var (
 	Apps         map[string]*AppPlugin
 	Plugins      map[string]*PluginBase
 	PluginTypes  map[string]interface{}
+
+	GrafanaLatestVersion string
+	GrafanaHasUpdate     bool
 )
 
 type PluginScanner struct {
@@ -70,6 +73,7 @@ func Init() error {
 		app.initApp()
 	}
 
+	go StartPluginUpdateChecker()
 	return nil
 }
 

+ 119 - 0
pkg/plugins/update_checker.go

@@ -0,0 +1,119 @@
+package plugins
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/setting"
+)
+
+type GrafanaNetPlugin struct {
+	Slug    string `json:"slug"`
+	Version string `json:"version"`
+}
+
+type GithubLatest struct {
+	Stable  string `json:"stable"`
+	Testing string `json:"testing"`
+}
+
+func StartPluginUpdateChecker() {
+	if !setting.CheckForUpdates {
+		return
+	}
+
+	// do one check directly
+	go checkForUpdates()
+
+	ticker := time.NewTicker(time.Minute * 10)
+	for {
+		select {
+		case <-ticker.C:
+			checkForUpdates()
+		}
+	}
+}
+
+func getAllExternalPluginSlugs() string {
+	str := ""
+
+	for _, plug := range Plugins {
+		if plug.IsCorePlugin {
+			continue
+		}
+
+		str += plug.Id + ","
+	}
+
+	return str
+}
+
+func checkForUpdates() {
+	log.Trace("Checking for updates")
+
+	client := http.Client{Timeout: time.Duration(5 * time.Second)}
+
+	pluginSlugs := getAllExternalPluginSlugs()
+	resp, err := client.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
+
+	if err != nil {
+		log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error())
+		return
+	}
+
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		log.Trace("Update check failed, reading response from grafana.net, %v", err.Error())
+		return
+	}
+
+	gNetPlugins := []GrafanaNetPlugin{}
+	err = json.Unmarshal(body, &gNetPlugins)
+	if err != nil {
+		log.Trace("Failed to unmarshal plugin repo, reading response from grafana.net, %v", err.Error())
+		return
+	}
+
+	for _, plug := range Plugins {
+		for _, gplug := range gNetPlugins {
+			if gplug.Slug == plug.Id {
+				plug.GrafanaNetVersion = gplug.Version
+				plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
+			}
+		}
+	}
+
+	resp2, err := client.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
+	if err != nil {
+		log.Trace("Failed to get lates.json repo from github: %v", err.Error())
+		return
+	}
+
+	defer resp2.Body.Close()
+	body, err = ioutil.ReadAll(resp2.Body)
+	if err != nil {
+		log.Trace("Update check failed, reading response from github.net, %v", err.Error())
+		return
+	}
+
+	var githubLatest GithubLatest
+	err = json.Unmarshal(body, &githubLatest)
+	if err != nil {
+		log.Trace("Failed to unmarshal github latest, reading response from github: %v", err.Error())
+		return
+	}
+
+	if strings.Contains(setting.BuildVersion, "-") {
+		GrafanaLatestVersion = githubLatest.Testing
+		GrafanaHasUpdate = !strings.HasPrefix(setting.BuildVersion, githubLatest.Testing)
+	} else {
+		GrafanaLatestVersion = githubLatest.Stable
+		GrafanaHasUpdate = githubLatest.Stable != setting.BuildVersion
+	}
+}

+ 8 - 6
pkg/services/sqlstore/migrations/preferences_mig.go

@@ -10,12 +10,12 @@ func addPreferencesMigrations(mg *Migrator) {
 		Name: "preferences",
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
-			{Name: "org_id", Type: DB_Int, Nullable: true},
-			{Name: "user_id", Type: DB_NVarchar, Length: 255, Nullable: true},
+			{Name: "org_id", Type: DB_BigInt, Nullable: false},
+			{Name: "user_id", Type: DB_BigInt, Nullable: false},
 			{Name: "version", Type: DB_Int, Nullable: false},
-			{Name: "home_dashboard_id", Type: DB_BigInt, Nullable: true},
-			{Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: true},
-			{Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: true},
+			{Name: "home_dashboard_id", Type: DB_BigInt, Nullable: false},
+			{Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: false},
+			{Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 		},
@@ -25,6 +25,8 @@ func addPreferencesMigrations(mg *Migrator) {
 		},
 	}
 
+	mg.AddMigration("drop preferences table v3", NewDropTableMigration("preferences"))
+
 	// create table
-	mg.AddMigration("create preferences table v2", NewAddTableMigration(preferencesV2))
+	mg.AddMigration("create preferences table v3", NewAddTableMigration(preferencesV2))
 }

+ 1 - 6
pkg/services/sqlstore/stats.go

@@ -85,12 +85,7 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
       (
         SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` )
         FROM ` + dialect.Quote("star") + `
-      ) AS starred_db_count,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("user") + `
-        WHERE ` + dialect.Quote("is_admin") + ` = 1
-      ) AS grafana_admin_count
+      ) AS starred_db_count
       `
 
 	var stats m.AdminStats

+ 2 - 0
pkg/setting/setting.go

@@ -124,6 +124,7 @@ var (
 	appliedEnvOverrides          []string
 
 	ReportingEnabled   bool
+	CheckForUpdates    bool
 	GoogleAnalyticsId  string
 	GoogleTagManagerId string
 
@@ -475,6 +476,7 @@ func NewConfigContext(args *CommandLineArgs) error {
 
 	analytics := Cfg.Section("analytics")
 	ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
+	CheckForUpdates = analytics.Key("check_for_updates").MustBool(true)
 	GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String()
 	GoogleTagManagerId = analytics.Key("google_tag_manager_id").String()
 

+ 2 - 11
public/app/core/components/colorpicker.ts

@@ -7,10 +7,6 @@ import coreModule from 'app/core/core_module';
 
 var template = `
 <div class="graph-legend-popover">
-  <a class="drop-popopver-close" ng-click="ctrl.close();" href="" ng-hide="ctrl.autoClose">
-    <i class="fa fa-times-circle"></i>
-  </a>
-
   <div ng-show="ctrl.series" class="p-b-1">
     <label>Y Axis:</label>
     <button ng-click="ctrl.toggleAxis(yaxis);" class="btn btn-small"
@@ -31,7 +27,6 @@ var template = `
     ng-style="{color:color}"
     ng-click="ctrl.colorSelected(color);">&nbsp;</i>
   </p>
-
 </div>
 `;
 
@@ -51,21 +46,17 @@ export class ColorPickerCtrl {
   toggleAxis(yaxis) {
     this.$scope.toggleAxis();
 
-    if (!this.$scope.autoClose) {
+    if (this.$scope.autoClose) {
       this.$scope.dismiss();
     }
   }
 
   colorSelected(color) {
     this.$scope.colorSelected(color);
-    if (!this.$scope.autoClose) {
+    if (this.$scope.autoClose) {
       this.$scope.dismiss();
     }
   }
-
-  close() {
-    this.$scope.dismiss();
-  }
 }
 
 export function colorPicker() {

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

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

+ 16 - 7
public/app/core/components/info_popover.ts

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

+ 0 - 4
public/app/core/components/sidemenu/sidemenu.html

@@ -21,10 +21,6 @@
 					<i class="{{::menuItem.icon}}" ng-show="::menuItem.icon"></i>
 					{{::menuItem.text}}
 				</a>
-				<a ng-click="menuItem.click()" ng-show="::menuItem.click">
-					<i class="{{::menuItem.icon}}"></i>
-					{{::menuItem.text}}
-				</a>
 			</li>
 		</ul>
 	</li>

+ 2 - 9
public/app/core/components/sidemenu/sidemenu.ts

@@ -72,9 +72,8 @@ export class SideMenuCtrl {
        this.orgMenu.push({
          text: "Switch to " + org.name,
          icon: "fa fa-fw fa-random",
-         click: () => {
-           this.switchOrg(org.orgId);
-         }
+         url: this.getUrl('/profile/switch-org/' + org.orgId),
+         target: '_self'
        });
      });
 
@@ -83,12 +82,6 @@ export class SideMenuCtrl {
      }
    });
  }
-
- switchOrg(orgId) {
-   this.backendSrv.post('/api/user/using/' + orgId).then(() => {
-     window.location.href = `${config.appSubUrl}/`;
-   });
- };
 }
 
 export function sideMenuDirective() {

+ 9 - 23
public/app/core/components/switch.ts

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

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

@@ -39,7 +39,9 @@ function (angular, coreModule, config) {
     $scope.buildInfo = {
       version: config.buildInfo.version,
       commit: config.buildInfo.commit,
-      buildstamp: new Date(config.buildInfo.buildstamp * 1000)
+      buildstamp: new Date(config.buildInfo.buildstamp * 1000),
+      latestVersion: config.buildInfo.latestVersion,
+      hasUpdate: config.buildInfo.hasUpdate,
     };
 
     $scope.submit = function() {

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

@@ -169,7 +169,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
         return System.import(model.module).then(function(appModule) {
           return {
             baseUrl: model.baseUrl,
-            name: 'app-config-' + model.pluginId,
+            name: 'app-config-' + model.id,
             bindings: {appModel: "=", appEditCtrl: "="},
             attrs: {"app-model": "ctrl.model", "app-edit-ctrl": "ctrl"},
             Component: appModule.ConfigCtrl,

+ 4 - 1
public/app/core/services/popover_srv.ts

@@ -46,9 +46,12 @@ function popoverSrv($compile, $rootScope) {
     drop.on('close', () => {
       popoverScope.dismiss({fromDropClose: true});
       destroyDrop();
+      if (options.onClose) {
+        options.onClose();
+      }
     });
 
-    drop.open();
+    setTimeout(() => { drop.open(); }, 10);
   };
 }
 

+ 3 - 1
public/app/core/time_series2.ts

@@ -42,6 +42,7 @@ export default class TimeSeries {
   fillBelowTo: any;
   transform: any;
   flotpairs: any;
+  unit: any;
 
   constructor(opts) {
     this.datapoints = opts.datapoints;
@@ -52,6 +53,7 @@ export default class TimeSeries {
     this.valueFormater = kbn.valueFormats.none;
     this.stats = {};
     this.legend = true;
+    this.unit = opts.unit;
   }
 
   applySeriesOverrides(overrides) {
@@ -170,7 +172,7 @@ export default class TimeSeries {
   }
 
   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 (timestamp.length === 13 && (timestamp % 1000) !== 0) {

+ 8 - 29
public/app/core/utils/emitter.ts

@@ -1,6 +1,6 @@
 ///<reference path="../../headers/common.d.ts" />
 
-import {Subject} from 'vendor/npm/rxjs/Subject';
+import EventEmitter from 'eventemitter3';
 
 var hasOwnProp = {}.hasOwnProperty;
 
@@ -9,48 +9,27 @@ function createName(name) {
 }
 
 export class Emitter {
-  subjects: any;
+  emitter: any;
 
   constructor() {
-    this.subjects = {};
+    this.emitter = new EventEmitter();
   }
 
   emit(name, data?) {
-    var fnName = createName(name);
-    this.subjects[fnName] || (this.subjects[fnName] = new Subject());
-    this.subjects[fnName].next(data);
+    this.emitter.emit(name, data);
   }
 
   on(name, handler, scope?) {
-    var fnName = createName(name);
-    this.subjects[fnName] || (this.subjects[fnName] = new Subject());
-    var subscription = this.subjects[fnName].subscribe(handler);
+    this.emitter.on(name, handler);
 
     if (scope) {
       scope.$on('$destroy', function() {
-        subscription.unsubscribe();
+        this.emitter.off(name, handler);
       });
     }
-
-    return subscription;
-  };
-
-  off(name, handler) {
-    var fnName = createName(name);
-    if (this.subjects[fnName]) {
-      this.subjects[fnName].dispose();
-      delete this.subjects[fnName];
-    }
   }
 
-  dispose() {
-    var subjects = this.subjects;
-    for (var prop in subjects) {
-      if (hasOwnProp.call(subjects, prop)) {
-        subjects[prop].dispose();
-      }
-    }
-
-    this.subjects = {};
+  off(name, handler) {
+    this.emitter.off(name, handler);
   }
 }

+ 0 - 4
public/app/features/admin/partials/stats.html

@@ -22,10 +22,6 @@
 				<td>Total users</td>
 				<td>{{ctrl.stats.user_count}}</td>
 			</tr>
-			<tr>
-				<td>Total grafana admins</td>
-				<td>{{ctrl.stats.grafana_admin_count}}</td>
-			</tr>
 			<tr>
 				<td>Total organizations</td>
 				<td>{{ctrl.stats.org_count}}</td>

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

@@ -45,6 +45,7 @@ function (angular, _, $) {
 
     $scope.reset = function() {
       $scope.currentAnnotation = angular.copy(annotationDefaults);
+      $scope.currentAnnotation.datasource = $scope.datasources[0].name;
       $scope.currentIsNew = true;
       $scope.datasourceChanged();
     };

+ 2 - 0
public/app/features/dashboard/dashboardSrv.js

@@ -428,6 +428,8 @@ function (angular, $, _, moment) {
         // update graph yaxes changes
         panelUpgrades.push(function(panel) {
           if (panel.type !== 'graph') { return; }
+          if (!panel.grid) { return; }
+
           if (!panel.yaxes) {
             panel.yaxes = [
               {

+ 1 - 2
public/app/features/dashboard/dashnav/dashnav.html

@@ -36,7 +36,7 @@
 		</ul>
 	</li>
 	<li ng-show="::dashboardMeta.canSave">
-		<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>
+		<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
 	</li>
 	<li ng-if="::showSettingsMenu" class="dropdown">
 		<a class="pointer" ng-click="hideTooltip($event)" bs-tooltip="'Manage dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
@@ -48,7 +48,6 @@
 			<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="editJson();">View JSON</a></li>
 			<li ng-if="contextSrv.isEditor && !dashboard.editable"><a class="pointer" ng-click="makeEditable();">Make Editable</a></li>
 			<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
-      <li ng-if="dashboard.id"><a class="pointer" ng-click="saveDashboardAsHome();">Set As Home</a></li>
 			<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
 		</ul>
 	</li>

+ 8 - 0
public/app/features/dashboard/keybindings.js

@@ -60,6 +60,14 @@ function(angular, $) {
         scope.appEvent('zoom-out', evt);
       }, { inputDisabled: true });
 
+      keyboardManager.bind('left', function(evt) {
+        scope.appEvent('shift-time-backward', evt);
+      }, { inputDisabled: true });
+
+      keyboardManager.bind('right', function(evt) {
+        scope.appEvent('shift-time-forward', evt);
+      }, { inputDisabled: true });
+
       keyboardManager.bind('ctrl+e', function(evt) {
         scope.appEvent('export-dashboard', evt);
       }, { inputDisabled: true });

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

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

+ 24 - 28
public/app/features/dashboard/partials/shareModal.html

@@ -38,31 +38,26 @@
 
 	<div ng-include src="'shareLinkOptions.html'"></div>
 
-	<div class="gf-form-group position-center">
-		<div class="gf-form width-30" >
+	<div class="gf-form-group section">
+		<div class="gf-form width-30">
 			<textarea rows="5" data-share-panel-url class="gf-form-input width-30" ng-model='iframeHtml'></textarea>
 		</div>
 	</div>
-	<div class="gf-form-group">
-		<div class="gf-form position-center">
-			<button class="btn btn-inverse" data-clipboard-text="{{iframeHtml}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
-		</div>
-	</div>
 </script>
 
 <script type="text/ng-template" id="shareLinkOptions.html">
-	<div class="gf-form-group position-center">
-		<div class="gf-form">
-			<span class="gf-form-label width-5">Include</span>
-			<editor-checkbox text="Current time range" model="options.forCurrent" change="buildUrl()"></editor-checkbox>
-		</div>
-		<div class="gf-form">
-			<span class="gf-form-label width-5">Include</span>
-			<editor-checkbox text="Template variables" model="options.includeTemplateVars" change="buildUrl()"></editor-checkbox>
-		</div>
+	<div class="gf-form-group section">
+		<gf-form-switch class="gf-form"
+			label="Current time range" label-class="width-12" switch-class="max-width-6"
+			checked="options.forCurrent" on-change="buildUrl()">
+		</gf-form-switch>
+		<gf-form-switch class="gf-form"
+			label="Template variables" label-class="width-12" switch-class="max-width-6"
+			checked="options.includeTemplateVars" on-change="buildUrl()">
+		</gf-form-switch>
 		<div class="gf-form">
-			<span class="gf-form-label width-5">Theme</span>
-			<div class="gf-form-select-wrapper max-width-10">
+			<span class="gf-form-label width-12">Theme</span>
+			<div class="gf-form-select-wrapper width-6">
 				<select class="gf-form-input" ng-model="options.theme" ng-options="f as f for f in ['current', 'dark', 'light']" ng-change="buildUrl()"></select>
 			</div>
 		</div>
@@ -75,18 +70,19 @@
 	</div>
 
 	<div ng-include src="'shareLinkOptions.html'"></div>
-	<div class="gf-form-group position-center">
-		<div class="gf-form-inline">
-
-			<div class="gf-form width-30">
-				<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
-			</div>
-			<div class="gf-form pull-right">
-				<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
+	<div>
+		<div class="gf-form-group section">
+			<div class="gf-form-inline">
+				<div class="gf-form width-30">
+					<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
+				</div>
+				<div class="gf-form pull-right">
+					<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
+				</div>
 			</div>
 		</div>
 	</div>
-	<div class="gf-form position-center" ng-show="modeSharePanel">
+	<div class="gf-form section" ng-show="modeSharePanel">
 		<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
 	</div>
 </script>
@@ -117,7 +113,7 @@
 			</p>
 		</div>
 
-		<div class="gf-form-group share-modal-options position-center">
+		<div class="gf-form-group share-modal-options">
 			<div class="gf-form" ng-if="step === 1">
 				<span class="gf-form-label width-12">Snapshot name</span>
 				<input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >

+ 8 - 4
public/app/features/dashboard/timepicker/timepicker.html

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

+ 26 - 0
public/app/features/dashboard/timepicker/timepicker.ts

@@ -30,6 +30,8 @@ export class TimePickerCtrl {
     $scope.ctrl = this;
 
     $rootScope.onAppEvent('zoom-out', () => this.zoom(2), $scope);
+    $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
+    $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
     $rootScope.onAppEvent('refresh', () => this.init(), $scope);
     $rootScope.onAppEvent('dash-editor-hidden', () => this.isOpen = false, $scope);
 
@@ -87,6 +89,30 @@ export class TimePickerCtrl {
     this.timeSrv.setTime({from: moment.utc(from), to: moment.utc(to) });
   }
 
+  move(direction) {
+    var range = this.timeSrv.timeRange();
+
+    var timespan = (range.to.valueOf() - range.from.valueOf()) / 2;
+    var to, from;
+    if (direction === -1) {
+      to = range.to.valueOf() - timespan;
+      from = range.from.valueOf() - timespan;
+    } else if (direction === 1) {
+      to = range.to.valueOf() + timespan;
+      from = range.from.valueOf() + timespan;
+      if (to > Date.now() && range.to < Date.now()) {
+        to = Date.now();
+        from = range.from.valueOf();
+      }
+    } else {
+      to = range.to.valueOf();
+      from = range.from.valueOf();
+    }
+
+    this.timeSrv.setTime({from: moment.utc(from), to: moment.utc(to) });
+
+  }
+
   openDropdown() {
     this.init();
     this.isOpen = true;

+ 0 - 1
public/app/features/dashboard/unsavedChangesSrv.js

@@ -102,7 +102,6 @@ function(angular, _) {
         value.current = null;
         value.options = null;
       });
-
     };
 
     p.hasChanges = function() {

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

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

+ 8 - 4
public/app/features/panel/metrics_panel_ctrl.ts

@@ -65,7 +65,7 @@ class MetricsPanelCtrl extends PanelCtrl {
       var data = this.panel.snapshotData;
       // backward compatability
       if (!_.isArray(data)) {
-        data = data;
+        data = data.data;
       }
 
       this.events.emit('data-snapshot-load', data);
@@ -82,6 +82,7 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.loading = true;
 
     // load datasource service
+    this.setTimeQueryStart();
     this.datasourceSrv.get(this.panel.datasource)
     .then(this.issueQueries.bind(this))
     .then(this.handleQueryResult.bind(this))
@@ -183,7 +184,6 @@ class MetricsPanelCtrl extends PanelCtrl {
       cacheTimeout: this.panel.cacheTimeout
     };
 
-    this.setTimeQueryStart();
     return datasource.query(metricsQuery);
   }
 
@@ -251,8 +251,12 @@ class MetricsPanelCtrl extends PanelCtrl {
   }
 
   addDataQuery(datasource) {
-    var target = {
-    };
+    var target: any = {};
+
+    if (datasource) {
+      target.datasource = datasource.name;
+    }
+
     this.panel.targets.push(target);
   }
 }

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

@@ -46,6 +46,7 @@ export class PanelCtrl {
 
     $scope.$on("refresh", () => this.refresh());
     $scope.$on("render", () => this.render());
+    $scope.$on("$destroy", () => this.events.emit('panel-teardown'));
   }
 
   init() {

+ 1 - 1
public/app/features/panel/partials/panelTime.html

@@ -31,7 +31,7 @@
 		</div>
 		<gf-form-switch class="gf-form max-width-30"
 			label="Hide time override info" label-class="width-12"
-			checked="ctrl.panel.hideTimeOverride" switch-class="max-width-6" on-change="ctrl.render()">
+			checked="ctrl.panel.hideTimeOverride" switch-class="max-width-6" on-change="ctrl.refresh()">
 		</gf-form-switch>
 	</div>
 </div>

+ 10 - 3
public/app/features/plugins/ds_edit_ctrl.ts

@@ -16,6 +16,8 @@ var defaults = {
   jsonData: {}
 };
 
+var datasourceCreated = false;
+
 export class DataSourceEditCtrl {
   isNew: boolean;
   datasources: any[];
@@ -66,6 +68,11 @@ export class DataSourceEditCtrl {
       this.backendSrv.get('/api/datasources/' + id).then(ds => {
         this.isNew = false;
         this.current = ds;
+
+        if (datasourceCreated) {
+          datasourceCreated = false;
+          this.testDatasource();
+        }
         return this.typeChanged();
       });
     }
@@ -123,14 +130,14 @@ export class DataSourceEditCtrl {
       if (this.current.id) {
         return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(() => {
           this.updateFrontendSettings().then(() => {
-            if (test) {
-              this.testDatasource();
-            }
+            this.testDatasource();
           });
         });
       } else {
         return this.backendSrv.post('/api/datasources', this.current).then(result => {
           this.updateFrontendSettings();
+
+          datasourceCreated = true;
           this.$location.path('datasources/edit/' + result.id);
         });
       }

+ 8 - 12
public/app/features/plugins/partials/ds_edit.html

@@ -25,24 +25,24 @@
 
   <div ng-if="ctrl.tabIndex === 0" class="tab-content">
 
-    <form name="ctrl.editForm">
+    <form name="ctrl.editForm" ng-if="ctrl.current">
       <div class="gf-form-group">
         <div class="gf-form-inline">
 					<div class="gf-form">
 						<span class="gf-form-label width-7">Name</span>
 						<input class="gf-form-input max-width-21" type="text" ng-model="ctrl.current.name" placeholder="My data source name" required>
+						<info-popover offset="0px -135px" mode="right-absolute">
+							The name is used when you select the data source in panels.
+							The <em>Default</em> data source is preselected in new
+							panels.
+						</info-popover>
 					</div>
-					<info-popover offset="0px -130px">
-						The name is used when you select the data source in panels.
-						The <code>Default</code> data source is preselected in new
-						panels.
-					</info-popover>
 					<gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
 				</div>
 
 				<div class="gf-form">
 					<span class="gf-form-label width-7">Type</span>
-					<div class="gf-form-select-wrapper max-width-21">
+					<div class="gf-form-select-wrapper max-width-23">
 						<select class="gf-form-input" ng-model="ctrl.current.type" ng-options="v.id as v.name for v in ctrl.types" ng-change="ctrl.typeChanged()"></select>
 					</div>
 				</div>
@@ -55,7 +55,6 @@
 
 			<div ng-if="ctrl.testing" style="margin-top: 25px">
 				<h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
-				<h5 ng-show="ctrl.testing.done">Test results</h5>
 				<div class="alert-{{ctrl.testing.status}} alert">
 					<div class="alert-title">{{ctrl.testing.title}}</div>
 					<div ng-bind='ctrl.testing.message'></div>
@@ -64,10 +63,7 @@
 
 			<div class="gf-form-button-row">
 				<button type="submit" class="btn btn-success" ng-show="ctrl.isNew" ng-click="ctrl.saveChanges()">Add</button>
-				<button type="submit" class="btn btn-success" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges()">Save</button>
-				<button type="submit" class="btn btn-secondary" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges(true)">
-					Test Connection
-				</button>
+				<button type="submit" class="btn btn-success" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges()">Save &amp; Test</button>
 				<button type="submit" class="btn btn-danger" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">
 					Delete
 				</button>

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

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

+ 3 - 2
public/app/features/plugins/partials/ds_list.html

@@ -20,8 +20,9 @@
 			<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
 				<a class="card-item" href="datasources/edit/{{ds.id}}/">
 					<div class="card-item-header">
-						<i class="icon-gf icon-gf-{{ds.type}}"></i>
-						{{ds.type}}
+						<div class="card-item-type">
+							{{ds.type}}
+						</div>
 					</div>
 					<div class="card-item-body">
 						<figure class="card-item-figure">

+ 5 - 2
public/app/features/plugins/partials/plugin_edit.html

@@ -4,8 +4,8 @@
 <div class="page-container" ng-init="ctrl.init()">
   <div class="page-header">
     <div class="plugin-header">
-      <span ng-show="ctrl.model.info.logos.large" class="plugin-header-logo">
-        <img src="{{ctrl.model.info.logos.large}}">
+      <span class="plugin-header-logo">
+        <img ng-src="{{ctrl.model.info.logos.large}}">
       </span>
 
       <div class="plugin-header-info-block">
@@ -55,6 +55,9 @@
       <section class="page-sidebar-section">
         <h4>Version</h4>
         <span>{{ctrl.model.info.version}}</span>
+				<div ng-show="ctrl.model.hasUpdate">
+          <a ng-click="ctrl.updateAvailable()" bs-tooltip="ctrl.model.latestVersion">Update Available!</a>
+				</div>
       </section>
       <section class="page-sidebar-section" ng-show="ctrl.model.type === 'app'">
         <h5>Includes</h4>

+ 7 - 2
public/app/features/plugins/partials/plugin_list.html

@@ -33,8 +33,13 @@
 			<li class="card-item-wrapper" ng-repeat="plugin in ctrl.plugins">
 				<a class="card-item" href="plugins/{{plugin.id}}/edit">
 					<div class="card-item-header">
-						<i class="icon-gf icon-gf-{{plugin.type}}"></i>
-						{{plugin.type}}
+						<div class="card-item-type">
+							<i class="icon-gf icon-gf-{{plugin.type}}"></i>
+							{{plugin.type}}
+						</div>
+					  <div class="card-item-notice" ng-show="plugin.hasUpdate">
+							<span bs-tooltip="plugin.latestVersion">Update available!</span>
+						</div>
 					</div>
 					<div class="card-item-body">
 						<figure class="card-item-figure">

+ 21 - 0
public/app/features/plugins/partials/update_instructions.html

@@ -0,0 +1,21 @@
+<div class="modal-body">
+	<div class="modal-header">
+		<h2 class="modal-header-title">
+			<i class="fa fa-cloud-download"></i>
+			<span class="p-l-1">Update Plugin</span>
+		</h2>
+
+		<a class="modal-header-close" ng-click="dismiss();">
+			<i class="fa fa-remove"></i>
+		</a>
+	</div>
+
+	<div class="modal-content">
+		<div class="gf-form-group">
+			<p>Type the following on the command line to update {{plugin.name}}.</p>
+			<pre><code>grafana-cli plugins update {{plugin.id}}</code></pre>
+			<span class="small">Check out {{plugin.name}} on <a href="http://grafana/net/plugins/{{plugin.id}}">Grafana.net</a> for README and changelog. If you do not have access to the command line, ask your Grafana administator.</span>
+		</div>
+		<p class="pluginlist-none-installed code--line"><img class="pluginlist-inline-logo" src="public/img/grafana_icon.svg"><strong>Pro tip</strong>: To update all plugins at once, type <code class="code--small">grafana-cli plugins update-all</code> on the command line.</div>
+	</div>
+</div>

+ 13 - 2
public/app/features/plugins/plugin_edit_ctrl.ts

@@ -19,6 +19,7 @@ export class PluginEditCtrl {
 
   /** @ngInject */
   constructor(private $scope,
+              private $rootScope,
               private backendSrv,
               private $routeParams,
               private $sce,
@@ -47,6 +48,7 @@ export class PluginEditCtrl {
       });
 
       if (this.model.type === 'app') {
+        this.tabIndex = 1;
         this.tabs.push('Config');
 
         this.hasDashboards = _.findWhere(result.includes, {type: 'dashboard'});
@@ -73,7 +75,7 @@ export class PluginEditCtrl {
       case 'datasource':  return 'icon-gf icon-gf-datasources';
       case 'panel':  return 'icon-gf icon-gf-panel';
       case 'app':  return 'icon-gf icon-gf-apps';
-      case 'page':  return 'icon-gf icon-gf-share';
+      case 'page':  return 'icon-gf icon-gf-endpoint-tiny';
       case 'dashboard':  return 'icon-gf icon-gf-dashboard';
     }
   }
@@ -128,6 +130,16 @@ export class PluginEditCtrl {
     this.postUpdateHook = callback;
   }
 
+  updateAvailable() {
+    var modalScope = this.$scope.$new(true);
+    modalScope.plugin = this.model;
+
+    this.$rootScope.appEvent('show-modal', {
+      src: 'public/app/features/plugins/partials/update_instructions.html',
+      scope: modalScope
+    });
+  }
+
   enable() {
     this.model.enabled = true;
     this.model.pinned = true;
@@ -142,4 +154,3 @@ export class PluginEditCtrl {
 }
 
 angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl);
-

+ 51 - 50
public/app/features/templating/partials/editor.html

@@ -179,55 +179,56 @@
 
 			<div class="section gf-form-group" >
 				<h5 class="section-heading">Selection Options</h5>
-        <gf-form-switch class="gf-form"
-                        label="Multi-value"
-                        label-class="width-10"
-                        tooltip="Enables multiple values to be selected at the same time"
-                        checked="current.multi"
-                        on-change="runQuery()">
-        </gf-form-switch>
-        <gf-form-switch class="gf-form"
-                        label="Include All option"
-                        label-class="width-10"
-                        checked="current.includeAll"
-                        on-change="runQuery()">
-        </gf-form-switch>
-
-				<div class="gf-form" ng-if="current.includeAll">
-					<span class="gf-form-label width-10">Custom all value</span>
-					<input type="text" class="gf-form-input max-width-15" ng-model='current.allValue' placeholder="blank = auto"></input>
-				</div>
-			</div>
-
-			<div class="gf-form-group" ng-if="current.type === 'query'">
-				<h5>Value groups/tags (Experimental feature)</h5>
-				<div class="gf-form">
-					<editor-checkbox text="Enable" model="current.useTags" change="runQuery()"></editor-checkbox>
-				</div>
-				<div class="gf-form last" ng-if="current.useTags">
-					<span class="gf-form-label width-10">Tags query</span>
-					<input type="text" class="gf-form-input" ng-model='current.tagsQuery' placeholder="metric name or tags query" ng-model-onblur></input>
-				</div>
-				<div class="gf-form" ng-if="current.useTags">
-					<li class="gf-form-label width-10">Tag values query</li>
-					<input type="text" class="gf-form-input" ng-model='current.tagValuesQuery' placeholder="apps.$tag.*" ng-model-onblur></input>
-				</div>
-			</div>
-
-			<div class="gf-form-group">
-				<h5>Preview of values (shows max 20)</h5>
-				<div class="gf-form">
-					<span class="gf-form-label" ng-repeat="option in current.options | limitTo: 20">
-						{{option.text}}
-					</span>
-				</div>
-			</div>
-		</div>
-
-		<div class="gf-form-button-row p-y-0">
-			<button type="button" class="btn btn-success" ng-show="mode === 'edit'" ng-click="update();">Update</button>
-			<button type="button" class="btn btn-success" ng-show="mode === 'new'" ng-click="add();">Add</button>
-		</div>
-	</div>
+        <div class="section">
+          <gf-form-switch class="gf-form"
+                          label="Multi-value"
+                          label-class="width-10"
+                          tooltip="Enables multiple values to be selected at the same time"
+                          checked="current.multi"
+                          on-change="runQuery()">
+          </gf-form-switch>
+          <gf-form-switch class="gf-form"
+                          label="Include All option"
+                          label-class="width-10"
+                          checked="current.includeAll"
+                          on-change="runQuery()">
+          </gf-form-switch>
+        </div>
+        <div class="gf-form" ng-if="current.includeAll">
+          <span class="gf-form-label width-10">Custom all value</span>
+          <input type="text" class="gf-form-input max-width-15" ng-model='current.allValue' placeholder="blank = auto"></input>
+        </div>
+      </div>
+
+      <div class="gf-form-group" ng-if="current.type === 'query'">
+        <h5>Value groups/tags (Experimental feature)</h5>
+        <div class="gf-form">
+          <editor-checkbox text="Enable" model="current.useTags" change="runQuery()"></editor-checkbox>
+        </div>
+        <div class="gf-form last" ng-if="current.useTags">
+          <span class="gf-form-label width-10">Tags query</span>
+          <input type="text" class="gf-form-input" ng-model='current.tagsQuery' placeholder="metric name or tags query" ng-model-onblur></input>
+        </div>
+        <div class="gf-form" ng-if="current.useTags">
+          <li class="gf-form-label width-10">Tag values query</li>
+          <input type="text" class="gf-form-input" ng-model='current.tagValuesQuery' placeholder="apps.$tag.*" ng-model-onblur></input>
+        </div>
+      </div>
+
+      <div class="gf-form-group">
+        <h5>Preview of values (shows max 20)</h5>
+        <div class="gf-form-inline">
+          <div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
+            <span class="gf-form-label">{{option.text}}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="gf-form-button-row p-y-0">
+      <button type="button" class="btn btn-success" ng-show="mode === 'edit'" ng-click="update();">Update</button>
+      <button type="button" class="btn btn-success" ng-show="mode === 'new'" ng-click="add();">Add</button>
+    </div>
+  </div>
 </div>
 

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

@@ -272,22 +272,24 @@ function (angular, _, kbn) {
       }
 
       for (i = 0; i < metricNames.length; i++) {
-        var value = metricNames[i].text;
+        var item = metricNames[i];
+        var value = item.value || item.text;
+        var text = item.text || item.value;
 
         if (regex) {
           matches = regex.exec(value);
           if (!matches) { continue; }
           if (matches.length > 1) {
             value = matches[1];
+            text = value;
           }
         }
 
-        options[value] = value;
+        options[value] = {text: text, value: value};
       }
 
       return _.map(_.keys(options).sort(), function(key) {
-        var option = { text: key, value: key };
-        return option;
+        return options[key];
       });
     };
 

+ 6 - 2
public/app/headers/common.d.ts

@@ -1,5 +1,4 @@
-///<reference path="../../vendor/npm/angular2/typings/es6-promise/es6-promise.d.ts" />
-///<reference path="../../vendor/npm/angular2/typings/es6-collections/es6-collections.d.ts" />
+/// <reference path="./es6-shim/es6-shim.d.ts" />
 
 declare var System: any;
 
@@ -48,3 +47,8 @@ declare module 'tether-drop' {
   var config: any;
   export default config;
 }
+
+declare module 'eventemitter3' {
+  var config: any;
+  export default config;
+}

+ 0 - 73
public/app/headers/es6-promise/es6-promise.d.ts

@@ -1,73 +0,0 @@
-// Type definitions for es6-promise
-// Project: https://github.com/jakearchibald/ES6-Promise
-// Definitions by: François de Campredon <https://github.com/fdecampredon/>, vvakame <https://github.com/vvakame>
-// Definitions: https://github.com/borisyankov/DefinitelyTyped
-
-interface Thenable<R> {
-    then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
-    then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => void): Thenable<U>;
-}
-
-declare class Promise<R> implements Thenable<R> {
-	/**
-	 * If you call resolve in the body of the callback passed to the constructor,
-	 * your promise is fulfilled with result object passed to resolve.
-	 * If you call reject your promise is rejected with the object passed to resolve.
-	 * For consistency and debugging (eg stack traces), obj should be an instanceof Error.
-	 * Any errors thrown in the constructor callback will be implicitly passed to reject().
-	 */
-	constructor(callback: (resolve : (value?: R | Thenable<R>) => void, reject: (error?: any) => void) => void);
-
-	/**
-	 * onFulfilled is called when/if "promise" resolves. onRejected is called when/if "promise" rejects.
-	 * Both are optional, if either/both are omitted the next onFulfilled/onRejected in the chain is called.
-	 * Both callbacks have a single parameter , the fulfillment value or rejection reason.
-	 * "then" returns a new promise equivalent to the value you return from onFulfilled/onRejected after being passed through Promise.resolve.
-	 * If an error is thrown in the callback, the returned promise rejects with that error.
-	 *
-	 * @param onFulfilled called when/if "promise" resolves
-	 * @param onRejected called when/if "promise" rejects
-	 */
-    then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Promise<U>;
-    then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => void): Promise<U>;
-
-	/**
-	 * Sugar for promise.then(undefined, onRejected)
-	 *
-	 * @param onRejected called when/if "promise" rejects
-	 */
-	catch<U>(onRejected?: (error: any) => U | Thenable<U>): Promise<U>;
-}
-
-declare module Promise {
-	/**
-	 * Make a new promise from the thenable.
-	 * A thenable is promise-like in as far as it has a "then" method.
-	 */
-	function resolve<R>(value?: R | Thenable<R>): Promise<R>;
-
-	/**
-	 * Make a promise that rejects to obj. For consistency and debugging (eg stack traces), obj should be an instanceof Error
-	 */
-	function reject(error: any): Promise<any>;
-
-	/**
-	 * Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects.
-	 * the array passed to all can be a mixture of promise-like objects and other objects.
-	 * The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value.
-	 */
-	function all<R>(promises: (R | Thenable<R>)[]): Promise<R[]>;
-
-	/**
-	 * Make a Promise that fulfills when any item fulfills, and rejects if any item rejects.
-	 */
-	function race<R>(promises: (R | Thenable<R>)[]): Promise<R>;
-}
-
-declare module 'es6-promise' {
-	var foo: typeof Promise; // Temp variable to reference Promise in local context
-	module rsvp {
-		export var Promise: typeof foo;
-	}
-	export = rsvp;
-}

+ 5 - 3
public/app/headers/es6-shim/es6-shim.d.ts

@@ -1,7 +1,9 @@
+// Generated by typings
+// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/es6-shim/es6-shim.d.ts
 // Type definitions for es6-shim v0.31.2
 // Project: https://github.com/paulmillr/es6-shim
 // Definitions by: Ron Buckton <http://github.com/rbuckton>
-// Definitions: https://github.com/borisyankov/DefinitelyTyped
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
 
 declare type PropertyKey = string | number | symbol;
 
@@ -621,7 +623,7 @@ interface WeakSetConstructor {
 
 declare var WeakSet: WeakSetConstructor;
 
-declare module Reflect {
+declare namespace Reflect {
     function apply(target: Function, thisArgument: any, argumentsList: ArrayLike<any>): any;
     function construct(target: Function, argumentsList: ArrayLike<any>): any;
     function defineProperty(target: any, propertyKey: PropertyKey, attributes: PropertyDescriptor): boolean;
@@ -649,7 +651,7 @@ declare module "es6-shim" {
     var WeakMap: WeakMapConstructor;
     var WeakSet: WeakSetConstructor;
     var Promise: PromiseConstructor;
-    module Reflect {
+    namespace Reflect {
         function apply(target: Function, thisArgument: any, argumentsList: ArrayLike<any>): any;
         function construct(target: Function, argumentsList: ArrayLike<any>): any;
         function defineProperty(target: any, propertyKey: PropertyKey, attributes: PropertyDescriptor): boolean;

+ 16 - 8
public/app/partials/help_modal.html

@@ -28,14 +28,22 @@
 				<td><span class="label label-info">R</span></td>
 				<td>Refresh (Fetches new data and rerenders panels)</td>
 			</tr>
+			<tr>
+				<td><span class="label label-info">left arrow key</span></td>
+				<td>Shift time backward</td>
+			</tr>
+			<tr>
+				<td><span class="label label-info">right arrow key</span></td>
+				<td>Shift time forward</td>
+			</tr>
 			<tr>
 				<td><span class="label label-info">CTRL+S</span></td>
 				<td>Save dashboard</td>
 			</tr>
-      <tr>
-        <td><span class="label label-info">CTRL+E</span></td>
-        <td>Export dashboard</td>
-      </tr>
+			<tr>
+				<td><span class="label label-info">CTRL+E</span></td>
+				<td>Export dashboard</td>
+			</tr>
 			<tr>
 				<td><span class="label label-info">CTRL+H</span></td>
 				<td>Hide row controls</td>
@@ -44,10 +52,10 @@
 				<td><span class="label label-info">CTRL+Z</span></td>
 				<td>Zoom out</td>
 			</tr>
-      <tr>
-        <td><span class="label label-info">CTRL+I</span></td>
-        <td>Quick snapshot</td>
-      </tr>
+			<tr>
+				<td><span class="label label-info">CTRL+I</span></td>
+				<td>Quick snapshot</td>
+			</tr>
 			<tr>
 				<td><span class="label label-info">CTRL+O</span></td>
 				<td>Enable/Disable shared graph crosshair</td>

+ 3 - 3
public/app/partials/login.html

@@ -78,9 +78,9 @@
 				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>
-
-

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

@@ -1,5 +1,6 @@
 <cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter>
-<div class="editor-row">
+
+<div class="editor-row" style="padding: 2rem 0">
 	<div class="section">
 		<h5>Prefix matching</h5>
 		<div class="editor-option">

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

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

+ 14 - 2
public/app/plugins/datasource/influxdb/datasource.ts

@@ -55,7 +55,7 @@ export default class InfluxDatasource {
       query = query.replace(/\$interval/g, (target.interval || options.interval));
       return query;
 
-    }).join("\n");
+    }).join(";");
 
     // replace grafana variables
     allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
@@ -107,7 +107,7 @@ export default class InfluxDatasource {
 
     var timeFilter = this.getTimeFilter({rangeRaw: options.rangeRaw});
     var query = options.annotation.query.replace('$timeFilter', timeFilter);
-    query = this.templateSrv.replace(query);
+    query = this.templateSrv.replace(query, null, 'regex');
 
     return this._seriesQuery(query).then(data => {
       if (!data || !data.results || !data.results[0]) {
@@ -133,6 +133,17 @@ export default class InfluxDatasource {
     return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
   }
 
+
+  serializeParams(params) {
+    if (!params) { return '';}
+
+    return _.reduce(params, (memo, value, key) => {
+      if (value === null || value === undefined) { return memo; }
+      memo.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
+      return memo;
+    }, []).join("&");
+  }
+
   testDatasource() {
     return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(() => {
       return { status: "success", message: "Data source is working", title: "Success" };
@@ -166,6 +177,7 @@ export default class InfluxDatasource {
       data:   data,
       precision: "ms",
       inspect: { type: 'influxdb' },
+      paramSerializer: this.serializeParams,
     };
 
     options.headers = options.headers || {};

+ 1 - 1
public/app/plugins/datasource/opentsdb/config_ctrl.ts

@@ -16,7 +16,7 @@ export class OpenTsConfigCtrl {
 
   tsdbVersions = [
     {name: '<=2.1', value: 1},
-    {name: '2.2', value: 2},
+    {name: '>=2.2', value: 2},
   ];
 
   tsdbResolutions = [

+ 1 - 1
public/app/plugins/datasource/prometheus/datasource.ts

@@ -157,7 +157,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
 
     var interpolated;
     try {
-      interpolated = templateSrv.replace(expr);
+      interpolated = templateSrv.replace(expr, {}, interpolateQueryExpr);
     } catch (err) {
       return $q.reject(err);
     }

+ 25 - 33
public/app/plugins/panel/dashlist/editor.html

@@ -1,40 +1,32 @@
-<div class="gf-form-group">
-	<div class="gf-form-inline">
-		<div class="gf-form">
-			<span class="gf-form-label width-10">Mode</span>
-			<div class="gf-form-select-wrapper max-width-10">
-				<select class="gf-form-input" ng-model="ctrl.panel.mode" ng-options="f for f in ctrl.modes" ng-change="ctrl.refresh()"></select>
-			</div>
-		</div>
-		<div class="gf-form" ng-show="ctrl.panel.mode === 'recently viewed'">
-			<span class="gf-form-label">
-				<i class="grafana-tip fa fa-question-circle ng-scope" bs-tooltip="'WARNING: This list will be cleared when clearing browser cache'" data-original-title="" title=""></i>
-			</span>
-		</div>
-	</div>
+<div>
+  <div class="section gf-form-group">
+    <h5 class="section-heading">Options</h5>
 
-	<div class="gf-form-inline" ng-if="ctrl.panel.mode === 'search'">
-		<div class="gf-form">
-			<span class="gf-form-label width-10">Search options</span>
-			<span class="gf-form-label">Query</span>
+    <gf-form-switch class="gf-form" label="Starred" label-class="width-9" checked="ctrl.panel.starred" on-change="ctrl.refresh()"></gf-form-switch>
+    <gf-form-switch class="gf-form" label="Recently viewed" label-class="width-9" checked="ctrl.panel.recent" on-change="ctrl.refresh()"></gf-form-switch>
+    <gf-form-switch class="gf-form" label="Search" label-class="width-9" checked="ctrl.panel.search" on-change="ctrl.refresh()"></gf-form-switch>
 
-			<input type="text" class="gf-form-input" placeholder="title query"
-				ng-model="ctrl.panel.query" ng-change="ctrl.refresh()" ng-model-onblur>
+    <gf-form-switch class="gf-form" label="Show headings" label-class="width-9" checked="ctrl.panel.headings" on-change="ctrl.refresh()"></gf-form-switch>
 
-		</div>
+    <div class="gf-form">
+      <span class="gf-form-label width-9">Max items</span>
+      <input class="gf-form-input max-width-5" type="number" ng-model="ctrl.panel.limit" ng-model-onblur ng-change="ctrl.refresh()">
+    </div>
+  </div>
 
-		<div class="gf-form">
-			<span class="gf-form-label">Tags</span>
+  <div class="section gf-form-group">
+    <h5 class="section-heading">Search</h5>
 
-			<bootstrap-tagsinput ng-model="ctrl.panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">
-			</bootstrap-tagsinput>
-		</div>
-	</div>
+    <div class="gf-form">
+      <span class="gf-form-label width-6">Query</span>
+      <input type="text" class="gf-form-input" placeholder="title query" ng-model="ctrl.panel.query" ng-change="ctrl.refresh()" ng-model-onblur>
+    </div>
+
+    <div class="gf-form">
+      <span class="gf-form-label width-6">Tags</span>
+      <bootstrap-tagsinput ng-model="ctrl.panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">
+      </bootstrap-tagsinput>
+    </div>
+  </div>
 
-	<div class="gf-form-inline">
-		<div class="gf-form">
-			<span class="gf-form-label width-10">Limit number to</span>
-			<input class="gf-form-input" type="number" ng-model="ctrl.panel.limit" ng-model-onblur ng-change="ctrl.refresh()">
-		</div>
-	</div>
 </div>

+ 16 - 11
public/app/plugins/panel/dashlist/module.html

@@ -1,12 +1,17 @@
-<div class="dashlist">
-	<div class="dashlist-item" ng-repeat="dash in ctrl.dashList">
-		<a class="dashlist-link dashlist-link-{{dash.type}}" href="dashboard/{{dash.uri}}">
-			<span class="dashlist-title">
-				{{dash.title}}
-			</span>
-			<span class="dashlist-star">
-				<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
-			</span>
-		</a>
-	</div>
+<div class="dashlist" ng-repeat="group in ctrl.groups">
+  <div class="dashlist-section" ng-if="group.show">
+    <h6 class="dashlist-section-header" ng-show="ctrl.panel.headings">
+      {{group.header}}
+    </h6>
+    <div class="dashlist-item" ng-repeat="dash in group.list">
+      <a class="dashlist-link dashlist-link-{{dash.type}}" href="dashboard/{{dash.uri}}">
+        <span class="dashlist-title">
+          {{dash.title}}
+        </span>
+        <span class="dashlist-star">
+          <i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
+        </span>
+      </a>
+    </div>
+  </div>
 </div>

+ 79 - 25
public/app/plugins/panel/dashlist/module.ts

@@ -7,16 +7,19 @@ import {impressions} from 'app/features/dashboard/impression_store';
 
  // Set and populate defaults
 var panelDefaults = {
-  mode: 'starred',
   query: '',
   limit: 10,
-  tags: []
+  tags: [],
+  recent: false,
+  search: false,
+  starred: true,
+  headings: true,
 };
 
 class DashListCtrl extends PanelCtrl {
   static templateUrl = 'module.html';
 
-  dashList: any[];
+  groups: any[];
   modes: any[];
 
   /** @ngInject */
@@ -31,6 +34,31 @@ class DashListCtrl extends PanelCtrl {
 
     this.events.on('refresh', this.onRefresh.bind(this));
     this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
+
+    this.groups = [
+      {list: [], show: false, header: "Starred dashboards",},
+      {list: [], show: false, header: "Recently viewed dashboards"},
+      {list: [], show: false, header: "Search"},
+    ];
+
+    // update capability
+    if (this.panel.mode) {
+      if (this.panel.mode === 'starred') {
+        this.panel.starred = true;
+        this.panel.headings = false;
+      }
+      if (this.panel.mode === 'recently viewed') {
+        this.panel.recent = true;
+        this.panel.starred = false;
+        this.panel.headings = false;
+      }
+      if (this.panel.mode === 'search') {
+        this.panel.search = true;
+        this.panel.starred = false;
+        this.panel.headings = false;
+      }
+      delete this.panel.mode;
+    }
   }
 
   onInitEditMode() {
@@ -40,34 +68,60 @@ class DashListCtrl extends PanelCtrl {
   }
 
   onRefresh() {
-    var params: any = {limit: this.panel.limit};
-
-    if (this.panel.mode === 'recently viewed') {
-      var dashIds = _.first(impressions.getDashboardOpened(), this.panel.limit);
-
-      return this.backendSrv.search({dashboardIds: dashIds, limit: this.panel.limit}).then(result => {
-        this.dashList = dashIds.map(orderId => {
-          return _.find(result, dashboard => {
-            return dashboard.id === orderId;
-          });
-        }).filter(el => {
-          return el !== undefined;
-        });
+    var promises = [];
 
-        this.renderingCompleted();
-      });
+    promises.push(this.getRecentDashboards());
+    promises.push(this.getStarred());
+    promises.push(this.getSearch());
+
+    return Promise.all(promises)
+      .then(this.renderingCompleted.bind(this));
+  }
+
+  getSearch() {
+    this.groups[2].show = this.panel.search;
+    if (!this.panel.search) {
+      return Promise.resolve();
     }
 
-    if (this.panel.mode === 'starred') {
-      params.starred = "true";
-    } else {
-      params.query = this.panel.query;
-      params.tag = this.panel.tags;
+    var params = {
+      limit: this.panel.limit,
+      query: this.panel.query,
+      tag: this.panel.tags,
+    };
+
+    return this.backendSrv.search(params).then(result => {
+      this.groups[2].list = result;
+    });
+  }
+
+  getStarred() {
+    this.groups[0].show = this.panel.starred;
+    if (!this.panel.starred) {
+      return Promise.resolve();
     }
 
+    var params = {limit: this.panel.limit, starred: "true"};
     return this.backendSrv.search(params).then(result => {
-      this.dashList = result;
-      this.renderingCompleted();
+      this.groups[0].list = result;
+    });
+  }
+
+  getRecentDashboards() {
+    this.groups[1].show = this.panel.recent;
+    if (!this.panel.recent) {
+      return Promise.resolve();
+    }
+
+    var dashIds = _.first(impressions.getDashboardOpened(), this.panel.limit);
+    return this.backendSrv.search({dashboardIds: dashIds, limit: this.panel.limit}).then(result => {
+      this.groups[1].list = dashIds.map(orderId => {
+        return _.find(result, dashboard => {
+          return dashboard.id === orderId;
+        });
+      }).filter(el => {
+        return el !== undefined;
+      });
     });
   }
 }

+ 4 - 2
public/app/plugins/panel/graph/graph.js

@@ -151,8 +151,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
         }
 
         function processOffsetHook(plot, gridMargin) {
-          if (panel.yaxis) { gridMargin.left = 20; }
-          if (panel.rightYAxisLabel) { gridMargin.right = 20; }
+          var left = panel.yaxes[0];
+          var right = panel.yaxes[1];
+          if (left.show && left.label) { gridMargin.left = 20; }
+          if (right.show && right.label) { gridMargin.right = 20; }
         }
 
         // Function for rendering panel

+ 12 - 10
public/app/plugins/panel/graph/graph_tooltip.js

@@ -9,7 +9,7 @@ function ($) {
     var ctrl = scope.ctrl;
     var panel = ctrl.panel;
 
-    var $tooltip = $('<div id="tooltip">');
+    var $tooltip = $('<div id="tooltip" class="graph-tooltip">');
 
     this.findHoverIndexFromDataPoints = function(posX, series, last) {
       var ps = series.datapoints.pointsize;
@@ -33,9 +33,8 @@ function ($) {
       return j - 1;
     };
 
-    this.showTooltip = function(absoluteTime, relativeTime, innerHtml, pos) {
-      var body = '<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ absoluteTime +
-        ' <span class="tone-down">(' + relativeTime + ')</span></div> ';
+    this.showTooltip = function(absoluteTime, innerHtml, pos) {
+      var body = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>';
       body += innerHtml + '</div>';
       $tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY);
     };
@@ -109,7 +108,7 @@ function ($) {
       var plot = elem.data().plot;
       var plotData = plot.getData();
       var seriesList = getSeriesFn();
-      var group, value, absoluteTime, relativeTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
+      var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
 
       if (panel.tooltip.msResolution) {
         tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
@@ -132,7 +131,6 @@ function ($) {
 
         seriesHtml = '';
 
-        relativeTime = dashboard.getRelativeTime(seriesHoverInfo.time);
         absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
 
         for (i = 0; i < seriesHoverInfo.length; i++) {
@@ -142,17 +140,22 @@ function ($) {
             continue;
           }
 
+          var highlightClass = '';
+          if (item && i === item.seriesIndex) {
+            highlightClass = 'graph-tooltip-list-item--highlight';
+          }
+
           series = seriesList[i];
 
           value = series.formatValue(hoverInfo.value);
 
-          seriesHtml += '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
+          seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
           seriesHtml += '<i class="fa fa-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
           seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
           plot.highlight(i, hoverInfo.hoverIndex);
         }
 
-        self.showTooltip(absoluteTime, relativeTime, seriesHtml, pos);
+        self.showTooltip(absoluteTime, seriesHtml, pos);
       }
       // single series tooltip
       else if (item) {
@@ -169,12 +172,11 @@ function ($) {
 
         value = series.formatValue(value);
 
-        relativeTime = dashboard.getRelativeTime(item.datapoint[0]);
         absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
 
         group += '<div class="graph-tooltip-value">' + value + '</div>';
 
-        self.showTooltip(absoluteTime, relativeTime, group, pos);
+        self.showTooltip(absoluteTime, group, pos);
       }
       // no hit
       else {

+ 2 - 3
public/app/plugins/panel/graph/legend.js

@@ -49,7 +49,6 @@ function (angular, _, $) {
               position: 'bottom center',
               template: '<gf-color-picker></gf-color-picker>',
               model: {
-                autoClose: true,
                 series: series,
                 toggleAxis: function() {
                   ctrl.toggleAxis(series);
@@ -194,9 +193,9 @@ function (angular, _, $) {
             }
 
             var topPadding = 6;
-            $container.css("height", maxHeight - topPadding);
+            $container.css("max-height", maxHeight - topPadding);
           } else {
-            $container.css("height", "");
+            $container.css("max-height", "");
           }
         }
       }

+ 18 - 5
public/app/plugins/panel/graph/module.ts

@@ -5,6 +5,7 @@ import './legend';
 import './series_overrides_ctrl';
 
 import template from './template';
+import angular from 'angular';
 import moment from 'moment';
 import kbn from 'app/core/utils/kbn';
 import _ from 'lodash';
@@ -108,13 +109,14 @@ class GraphCtrl extends MetricsPanelCtrl {
   constructor($scope, $injector, private annotationsSrv) {
     super($scope, $injector);
 
-    _.defaults(this.panel, panelDefaults);
+    _.defaults(this.panel, angular.copy(panelDefaults));
     _.defaults(this.panel.tooltip, panelDefaults.tooltip);
     _.defaults(this.panel.grid, panelDefaults.grid);
     _.defaults(this.panel.legend, panelDefaults.legend);
 
     this.colors = $scope.$root.colors;
 
+    this.events.on('render', this.onRender.bind(this));
     this.events.on('data-received', this.onDataReceived.bind(this));
     this.events.on('data-error', this.onDataError.bind(this));
     this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this));
@@ -159,7 +161,7 @@ class GraphCtrl extends MetricsPanelCtrl {
 
   onDataSnapshotLoad(snapshotData) {
     this.annotationsPromise = this.annotationsSrv.getAnnotations(this.dashboard);
-    this.onDataReceived(snapshotData.data);
+    this.onDataReceived(snapshotData);
   }
 
   onDataError(err) {
@@ -200,6 +202,7 @@ class GraphCtrl extends MetricsPanelCtrl {
       datapoints: datapoints,
       alias: alias,
       color: color,
+      unit: seriesData.unit,
     });
 
     if (datapoints && datapoints.length > 0) {
@@ -210,14 +213,25 @@ class GraphCtrl extends MetricsPanelCtrl {
       }
 
       this.datapointsCount += datapoints.length;
-
       this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded();
     }
 
-    series.applySeriesOverrides(this.panel.seriesOverrides);
+
     return series;
   }
 
+  onRender() {
+    if (!this.seriesList) { return; }
+
+    for (let series of this.seriesList) {
+      series.applySeriesOverrides(this.panel.seriesOverrides);
+
+      if (series.unit) {
+        this.panel.yaxes[series.yaxis-1].format = series.unit;
+      }
+    }
+  }
+
   changeSeriesColor(series, color) {
     series.color = color;
     this.panel.aliasColors[series.alias] = series.color;
@@ -234,7 +248,6 @@ class GraphCtrl extends MetricsPanelCtrl {
     } else {
       this.toggleSeriesExclusiveMode(serie);
     }
-
     this.render();
   }
 

+ 4 - 0
public/app/plugins/panel/graph/series_overrides_ctrl.js

@@ -59,7 +59,11 @@ define([
         openOn: 'click',
         template: '<gf-color-picker></gf-color-picker>',
         model: {
+          autoClose: true,
           colorSelected: $scope.colorSelected,
+        },
+        onClose: function() {
+          $scope.ctrl.render();
         }
       });
     };

+ 1 - 1
public/app/plugins/panel/graph/tab_display.html

@@ -69,7 +69,7 @@
 		</gf-form-switch>
 		<gf-form-switch class="gf-form" ng-show="ctrl.panel.stack"
 			label="Percent" label-class="width-7"
-			checked="ctrl.panel.percent" on-change="ctrl.render()">
+			checked="ctrl.panel.percentage" on-change="ctrl.render()">
 		</gf-form-switch>
 		<div class="gf-form" ng-show="ctrl.panel.stack">
 			<label class="gf-form-label width-7">Tooltip value</label>

+ 1 - 2
public/app/plugins/panel/graph/tab_legend.html

@@ -13,7 +13,6 @@
 			label="To the right" label-class="width-7"
 			checked="ctrl.panel.legend.rightSide" on-change="ctrl.render()">
 		</gf-form-switch>
-
 		<div ng-if="ctrl.panel.legend.rightSide" class="gf-form">
 			<label class="gf-form-label width-7">Width</label>
 			<input type="number" class="gf-form-input max-width-5" placeholder="250" bs-tooltip="'Set a min-width for the legend side table/block'" data-placement="right" ng-model="ctrl.panel.legend.sideWidth" ng-change="ctrl.render()" ng-model-onblur>
@@ -31,7 +30,7 @@
 
 			<gf-form-switch class="gf-form max-width-12"
 				label="Max" label-class="width-6" switch-class="max-width-5"
-				checked="ctrl.panel.legend.max" on-change="ctrl.legendCaluesOptionChanged()">
+				checked="ctrl.panel.legend.max" on-change="ctrl.legendValuesOptionChanged()">
 			</gf-form-switch>
 		</div>
 

+ 2 - 0
public/app/plugins/panel/pluginlist/README.md

@@ -0,0 +1,2 @@
+# Plugin List Panel -  Native Plugin
+

+ 40 - 0
public/app/plugins/panel/pluginlist/editor.html

@@ -0,0 +1,40 @@
+<div class="gf-form-group">
+	<div class="gf-form-inline">
+		<div class="gf-form">
+			<span class="gf-form-label width-10">Mode</span>
+			<div class="gf-form-select-wrapper max-width-10">
+				<select class="gf-form-input" ng-model="ctrl.panel.mode" ng-options="f for f in ctrl.modes" ng-change="ctrl.refresh()"></select>
+			</div>
+		</div>
+		<div class="gf-form" ng-show="ctrl.panel.mode === 'recently viewed'">
+			<span class="gf-form-label">
+				<i class="grafana-tip fa fa-question-circle ng-scope" bs-tooltip="'WARNING: This list will be cleared when clearing browser cache'" data-original-title="" title=""></i>
+			</span>
+		</div>
+	</div>
+
+	<div class="gf-form-inline" ng-if="ctrl.panel.mode === 'search'">
+		<div class="gf-form">
+			<span class="gf-form-label width-10">Search options</span>
+			<span class="gf-form-label">Query</span>
+
+			<input type="text" class="gf-form-input" placeholder="title query"
+				ng-model="ctrl.panel.query" ng-change="ctrl.refresh()" ng-model-onblur>
+
+		</div>
+
+		<div class="gf-form">
+			<span class="gf-form-label">Tags</span>
+
+			<bootstrap-tagsinput ng-model="ctrl.panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">
+			</bootstrap-tagsinput>
+		</div>
+	</div>
+
+	<div class="gf-form-inline">
+		<div class="gf-form">
+			<span class="gf-form-label width-10">Limit number to</span>
+			<input class="gf-form-input" type="number" ng-model="ctrl.panel.limit" ng-model-onblur ng-change="ctrl.refresh()">
+		</div>
+	</div>
+</div>

+ 119 - 0
public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
+<g>
+	<g>
+		<path style="fill:#666666;" d="M8.842,11.219h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,11.219z"/>
+		<path style="fill:#666666;" d="M0.008,2.113l2.054-2.054C0.966,0.139,0.089,1.016,0.008,2.113z"/>
+		<polygon style="fill:#666666;" points="0,2.998 0,5.533 5.484,0.05 2.948,0.05 		"/>
+		<polygon style="fill:#666666;" points="6.361,0.05 0,6.411 0,8.946 8.896,0.05 		"/>
+		<path style="fill:#666666;" d="M11.169,2.277c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V2.277z"/>
+		<path style="fill:#666666;" d="M9.654,0.169L0.119,9.704c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,0.812,10.247,0.37,9.654,0.169z"/>
+		<polygon style="fill:#666666;" points="11.169,5.479 5.429,11.219 7.964,11.219 11.169,8.014 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,11.031H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,10.212,89.157,11.031,88.146,11.031z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,23.902h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,23.902z"/>
+		<path style="fill:#666666;" d="M0.008,14.796l2.054-2.054C0.966,12.822,0.089,13.699,0.008,14.796z"/>
+		<polygon style="fill:#666666;" points="0,15.681 0,18.216 5.484,12.733 2.948,12.733 		"/>
+		<polygon style="fill:#666666;" points="6.361,12.733 0,19.094 0,21.629 8.896,12.733 		"/>
+		<path style="fill:#666666;" d="M11.169,14.96c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V14.96z"/>
+		<path style="fill:#666666;" d="M9.654,12.852l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,13.495,10.247,13.053,9.654,12.852z"/>
+		<polygon style="fill:#666666;" points="11.169,18.162 5.429,23.902 7.964,23.902 11.169,20.697 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,23.714H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,22.895,89.157,23.714,88.146,23.714z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,36.585h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,36.585z"/>
+		<path style="fill:#666666;" d="M0.008,27.479l2.054-2.054C0.966,25.505,0.089,26.382,0.008,27.479z"/>
+		<polygon style="fill:#666666;" points="0,28.364 0,30.899 5.484,25.416 2.948,25.416 		"/>
+		<polygon style="fill:#666666;" points="6.361,25.416 0,31.777 0,34.312 8.896,25.416 		"/>
+		<path style="fill:#666666;" d="M11.169,27.643c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V27.643z"/>
+		<path style="fill:#666666;" d="M9.654,25.535L0.119,35.07c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,26.178,10.247,25.736,9.654,25.535z"/>
+		<polygon style="fill:#666666;" points="11.169,30.845 5.429,36.585 7.964,36.585 11.169,33.38 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,36.397H14.866c-1.011,0-1.83-0.82-1.83-1.831v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,35.578,89.157,36.397,88.146,36.397z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,49.268h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,49.268z"/>
+		<path style="fill:#666666;" d="M0.008,40.162l2.054-2.054C0.966,38.188,0.089,39.065,0.008,40.162z"/>
+		<polygon style="fill:#666666;" points="0,41.047 0,43.582 5.484,38.099 2.948,38.099 		"/>
+		<polygon style="fill:#666666;" points="6.361,38.099 0,44.46 0,46.995 8.896,38.099 		"/>
+		<path style="fill:#666666;" d="M11.169,40.326c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V40.326z"/>
+		<path style="fill:#666666;" d="M9.654,38.218l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,38.861,10.247,38.419,9.654,38.218z"/>
+		<polygon style="fill:#666666;" points="11.169,43.528 5.429,49.268 7.964,49.268 11.169,46.063 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,49.08H14.866c-1.011,0-1.83-0.82-1.83-1.831v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,48.261,89.157,49.08,88.146,49.08z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,61.951h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,61.951z"/>
+		<path style="fill:#666666;" d="M0.008,52.845l2.054-2.054C0.966,50.871,0.089,51.748,0.008,52.845z"/>
+		<polygon style="fill:#666666;" points="0,53.73 0,56.265 5.484,50.782 2.948,50.782 		"/>
+		<polygon style="fill:#666666;" points="6.361,50.782 0,57.143 0,59.678 8.896,50.782 		"/>
+		<path style="fill:#666666;" d="M11.169,53.009c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V53.009z"/>
+		<path style="fill:#666666;" d="M9.654,50.901l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,51.544,10.247,51.102,9.654,50.901z"/>
+		<polygon style="fill:#666666;" points="11.169,56.211 5.429,61.951 7.964,61.951 11.169,58.746 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,61.763H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,60.944,89.157,61.763,88.146,61.763z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,74.634h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,74.634z"/>
+		<path style="fill:#666666;" d="M0.008,65.528l2.054-2.054C0.966,63.554,0.089,64.431,0.008,65.528z"/>
+		<polygon style="fill:#666666;" points="0,66.413 0,68.948 5.484,63.465 2.948,63.465 		"/>
+		<polygon style="fill:#666666;" points="6.361,63.465 0,69.826 0,72.361 8.896,63.465 		"/>
+		<path style="fill:#666666;" d="M11.169,65.692c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V65.692z"/>
+		<path style="fill:#666666;" d="M9.654,63.584l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,64.227,10.247,63.785,9.654,63.584z"/>
+		<polygon style="fill:#666666;" points="11.169,68.894 5.429,74.634 7.964,74.634 11.169,71.429 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,74.446H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,73.627,89.157,74.446,88.146,74.446z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,87.317h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,87.317z"/>
+		<path style="fill:#666666;" d="M0.008,78.211l2.054-2.054C0.966,76.237,0.089,77.114,0.008,78.211z"/>
+		<polygon style="fill:#666666;" points="0,79.096 0,81.631 5.484,76.148 2.948,76.148 		"/>
+		<polygon style="fill:#666666;" points="6.361,76.148 0,82.509 0,85.044 8.896,76.148 		"/>
+		<path style="fill:#666666;" d="M11.169,78.375c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V78.375z"/>
+		<path style="fill:#666666;" d="M9.654,76.267l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,76.91,10.247,76.468,9.654,76.267z"/>
+		<polygon style="fill:#666666;" points="11.169,81.577 5.429,87.317 7.964,87.317 11.169,84.112 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,87.129H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,86.31,89.157,87.129,88.146,87.129z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,100h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,100z"/>
+		<path style="fill:#666666;" d="M0.008,90.894l2.054-2.054C0.966,88.92,0.089,89.797,0.008,90.894z"/>
+		<polygon style="fill:#666666;" points="0,91.779 0,94.314 5.484,88.831 2.948,88.831 		"/>
+		<polygon style="fill:#666666;" points="6.361,88.831 0,95.192 0,97.727 8.896,88.831 		"/>
+		<path style="fill:#666666;" d="M11.169,91.058c0-0.068-0.004-0.134-0.01-0.2L2.027,99.99c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V91.058z"/>
+		<path style="fill:#666666;" d="M9.654,88.95l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,89.593,10.247,89.151,9.654,88.95z"/>
+		<polygon style="fill:#666666;" points="11.169,94.26 5.429,100 7.964,100 11.169,96.795 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,99.812H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,98.993,89.157,99.812,88.146,99.812z"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="5.637" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="18.37" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="31.104" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="43.837" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="56.57" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="69.304" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="82.037" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="94.77" r="3.875"/>
+</g>
+</svg>

+ 30 - 0
public/app/plugins/panel/pluginlist/module.html

@@ -0,0 +1,30 @@
+<div class="pluginlist">
+  <div class="pluginlist-section" ng-repeat="category in ctrl.viewModel">
+    <h6 class="pluginlist-section-header">
+      {{category.header}}
+    </h6>
+    <div class="pluginlist-item" ng-repeat="plugin in category.list">
+      <a class="pluginlist-link pluginlist-link-{{plugin.state}} pointer" href="plugins/{{plugin.id}}/edit">
+        <span>
+          <img ng-src="{{plugin.info.logos.small}}" class="pluginlist-image">
+          <span class="pluginlist-title">{{plugin.name}}</span>
+          <span class="pluginlist-version">v{{plugin.info.version}}</span>
+        </span>
+        <span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion">
+          Update available!
+        </span>
+        <span class="pluginlist-message pluginlist-message--enable" ng-show="!plugin.enabled && !plugin.hasUpdate">
+          Enable now
+        </span>
+        <span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate">
+          Up to date
+        </span>
+      </a>
+    </div>
+    <div class="pluginlist-item" ng-show="category.list.length === 0">
+      <a class="pluginlist-link pluginlist-link-{{plugin.state}}" href="http://grafana.net/plugins/">
+        <span class="pluginlist-none-installed">No additional panels installed. <span class="pluginlist-emphasis">Browse Grafana.net</span></span>
+      </a>
+    </div>
+  </div>
+</div>

+ 74 - 0
public/app/plugins/panel/pluginlist/module.ts

@@ -0,0 +1,74 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import _ from 'lodash';
+import config from 'app/core/config';
+import {PanelCtrl} from '../../../features/panel/panel_ctrl';
+
+// Set and populate defaults
+var panelDefaults = {
+};
+
+class PluginListCtrl extends PanelCtrl {
+  static templateUrl = 'module.html';
+
+  pluginList: any[];
+  viewModel: any;
+
+  /** @ngInject */
+  constructor($scope, $injector, private backendSrv, private $location) {
+    super($scope, $injector);
+    _.defaults(this.panel, panelDefaults);
+
+    this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
+    this.pluginList = [];
+    this.viewModel = [
+      {header: "Installed Apps", list: [], type: 'app'},
+      {header: "Installed Panels", list: [], type: 'panel'},
+      {header: "Installed Datasources", list: [], type: 'datasource'},
+    ];
+
+    this.update();
+  }
+
+  onInitEditMode() {
+    this.editorTabIndex = 1;
+    this.addEditorTab('Options', 'public/app/plugins/panel/pluginlist/editor.html');
+  }
+
+  gotoPlugin(plugin, evt) {
+    if (evt) { evt.stopPropagation(); }
+    this.$location.url(`plugins/${plugin.id}/edit`);
+  }
+
+  updateAvailable(plugin, $event) {
+    $event.stopPropagation();
+    $event.preventDefault();
+
+    var modalScope = this.$scope.$new(true);
+    modalScope.plugin = plugin;
+
+    this.publishAppEvent('show-modal', {
+      src: 'public/app/features/plugins/partials/update_instructions.html',
+      scope: modalScope
+    });
+  }
+
+  update() {
+    this.backendSrv.get('api/plugins', {embedded: 0, core: 0}).then(plugins => {
+      this.pluginList = plugins;
+      this.viewModel[0].list = _.filter(plugins, {type: 'app'});
+      this.viewModel[1].list = _.filter(plugins, {type: 'panel'});
+      this.viewModel[2].list = _.filter(plugins, {type: 'datasource'});
+
+      for (let plugin of this.pluginList) {
+        if (plugin.hasUpdate) {
+          plugin.state = 'has-update';
+        } else if (!plugin.enabled) {
+          plugin.state = 'not-enabled';
+        }
+      }
+    });
+  }
+}
+
+export {PluginListCtrl, PluginListCtrl as PanelCtrl}

+ 16 - 0
public/app/plugins/panel/pluginlist/plugin.json

@@ -0,0 +1,16 @@
+{
+  "type": "panel",
+  "name": "Plugin list",
+  "id": "pluginlist",
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "http://grafana.org"
+},
+    "logos": {
+      "small": "img/icn-dashlist-panel.svg",
+      "large": "img/icn-dashlist-panel.svg"
+    }
+  }
+}

+ 1 - 5
public/app/plugins/panel/singlestat/module.ts

@@ -56,7 +56,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
 
     this.events.on('data-received', this.onDataReceived.bind(this));
     this.events.on('data-error', this.onDataError.bind(this));
-    this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this));
+    this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
     this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
   }
 
@@ -71,10 +71,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     this.render();
   }
 
-  onDataSnapshotLoad(snapshotData) {
-    this.onDataReceived(snapshotData.data);
-  }
-
   onDataError(err) {
     this.onDataReceived({data: []});
   }

+ 4 - 7
public/app/plugins/panel/table/module.ts

@@ -60,7 +60,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
 
     this.events.on('data-received', this.onDataReceived.bind(this));
     this.events.on('data-error', this.onDataError.bind(this));
-    this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this));
+    this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
     this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
     this.events.on('init-panel-actions', this.onInitPanelActions.bind(this));
   }
@@ -77,19 +77,15 @@ class TablePanelCtrl extends MetricsPanelCtrl {
     this.pageIndex = 0;
 
     if (this.panel.transform === 'annotations') {
+      this.setTimeQueryStart();
       return this.annotationsSrv.getAnnotations(this.dashboard).then(annotations => {
-        this.dataRaw = annotations;
-        this.render();
+        return {data: annotations};
       });
     }
 
     return super.issueQueries(datasource);
   }
 
-  onDataSnapshotLoad(data) {
-    this.onDataReceived(data.data);
-  }
-
   onDataError(err) {
     this.dataRaw = [];
     this.render();
@@ -218,6 +214,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
       if (data) {
         renderPanel();
       }
+      ctrl.renderingCompleted();
     });
   }
 }

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä