Sfoglia il codice sorgente

Merge branch 'master' into elastic5_support

bergquist 9 anni fa
parent
commit
619c5c4f1b
76 ha cambiato i file con 1376 aggiunte e 506 eliminazioni
  1. 10 6
      CHANGELOG.md
  2. 1 1
      docs/sources/alerting/notifications.md
  3. 2 2
      docs/sources/http_api/org.md
  4. 3 3
      docs/sources/installation/debian.md
  5. 4 4
      docs/sources/installation/rpm.md
  6. 1 1
      docs/sources/installation/windows.md
  7. 4 4
      docs/sources/reference/export_import.md
  8. 1 1
      docs/sources/reference/singlestat.md
  9. 2 2
      latest.json
  10. 2 2
      packaging/publish/publish.sh
  11. 4 1
      pkg/api/api.go
  12. 20 0
      pkg/api/dashboard.go
  13. 1 74
      pkg/api/dataproxy.go
  14. 1 152
      pkg/api/dataproxy_test.go
  15. 2 3
      pkg/api/datasources.go
  16. 14 13
      pkg/api/dtos/models.go
  17. 7 6
      pkg/api/frontendsettings.go
  18. 1 0
      pkg/api/index.go
  19. 4 3
      pkg/api/metrics.go
  20. 31 0
      pkg/api/user.go
  21. 2 0
      pkg/metrics/metrics.go
  22. 4 0
      pkg/middleware/middleware.go
  23. 95 0
      pkg/models/datasource_cache.go
  24. 157 0
      pkg/models/datasource_cache_test.go
  25. 18 0
      pkg/models/helpflags.go
  26. 2 0
      pkg/models/user.go
  27. 1 0
      pkg/plugins/models.go
  28. 3 15
      pkg/services/alerting/conditions/query.go
  29. 19 19
      pkg/services/alerting/conditions/reducer_test.go
  30. 118 0
      pkg/services/alerting/notifiers/opsgenie.go
  31. 52 0
      pkg/services/alerting/notifiers/opsgenie_test.go
  32. 4 0
      pkg/services/sqlstore/migrations/user_mig.go
  33. 20 1
      pkg/services/sqlstore/user.go
  34. 4 7
      pkg/tsdb/batch.go
  35. 16 6
      pkg/tsdb/executor.go
  36. 7 3
      pkg/tsdb/fake_test.go
  37. 16 8
      pkg/tsdb/graphite/graphite.go
  38. 0 29
      pkg/tsdb/http.go
  39. 16 10
      pkg/tsdb/influxdb/influxdb.go
  40. 2 2
      pkg/tsdb/influxdb/model_parser.go
  41. 2 2
      pkg/tsdb/influxdb/model_parser_test.go
  42. 2 15
      pkg/tsdb/models.go
  43. 16 8
      pkg/tsdb/opentsdb/opentsdb.go
  44. 17 4
      pkg/tsdb/prometheus/prometheus.go
  45. 6 5
      pkg/tsdb/testdata/testdata.go
  46. 19 18
      pkg/tsdb/tsdb_test.go
  47. 3 1
      public/app/core/services/backend_srv.ts
  48. 1 0
      public/app/core/services/context_srv.ts
  49. 1 0
      public/app/core/services/keybindingSrv.ts
  50. 1 0
      public/app/features/alerting/alert_tab_ctrl.ts
  51. 18 1
      public/app/features/alerting/partials/notification_edit.html
  52. 4 4
      public/app/features/dashboard/import/dash_import.html
  53. 3 3
      public/app/features/dashboard/partials/settings.html
  54. 8 5
      public/app/features/dashboard/row/add_panel.ts
  55. 14 2
      public/app/features/plugins/ds_edit_ctrl.ts
  56. 2 2
      public/app/partials/dashboard.html
  57. 9 4
      public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html
  58. 5 0
      public/app/plugins/datasource/elasticsearch/partials/metric_agg.html
  59. 8 0
      public/app/plugins/datasource/elasticsearch/query_builder.js
  60. 2 1
      public/app/plugins/datasource/elasticsearch/query_def.js
  61. 2 0
      public/app/plugins/panel/gettingstarted/README.md
  62. 40 0
      public/app/plugins/panel/gettingstarted/editor.html
  63. 119 0
      public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg
  64. 19 0
      public/app/plugins/panel/gettingstarted/module.html
  65. 119 0
      public/app/plugins/panel/gettingstarted/module.ts
  66. 18 0
      public/app/plugins/panel/gettingstarted/plugin.json
  67. 31 30
      public/app/plugins/panel/graph/graph.ts
  68. 37 14
      public/app/plugins/panel/graph/graph_tooltip.js
  69. 1 2
      public/app/plugins/panel/graph/module.ts
  70. 1 1
      public/app/plugins/panel/graph/specs/graph_specs.ts
  71. 1 1
      public/app/plugins/panel/graph/tab_display.html
  72. 1 2
      public/app/plugins/panel/pluginlist/module.ts
  73. 2 2
      public/dashboards/home.json
  74. 1 0
      public/sass/_grafana.scss
  75. 171 0
      public/sass/components/_panel_gettingstarted.scss
  76. 1 1
      public/sass/components/_tabs.scss

+ 10 - 6
CHANGELOG.md

@@ -1,23 +1,27 @@
 # 4.1-beta (unreleased)
 # 4.1-beta (unreleased)
 
 
-### Bugfixes
-* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)
-* **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443)
-
 ### Enhancements
 ### Enhancements
 * **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655)
 * **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655)
-* **Victorops**: Add VictorOps Notification Integration [#6411](https://github.com/grafana/grafana/issues/6411)
+* **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411)
+* **Opsgenie**: Add OpsGenie notification integratiion (by [@kylemcc](https://github.com/kylemcc)) [#6687](https://github.com/grafana/grafana/issues/6687)
 * **Singlestat**: New aggregation on singlestat panel [#6740](https://github.com/grafana/grafana/pull/6740)
 * **Singlestat**: New aggregation on singlestat panel [#6740](https://github.com/grafana/grafana/pull/6740)
 * **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697)
 * **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697)
 * **Table**: Added Hidden Column Style for Table Panel [#5677](https://github.com/grafana/grafana/pull/5677)
 * **Table**: Added Hidden Column Style for Table Panel [#5677](https://github.com/grafana/grafana/pull/5677)
+* **Graph**: Shared crosshair option renamed to shared tooltip, shows tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274)
+* **Elasticsearch**: Added support for Missing option (bucket) for terms aggregation [#4244](https://github.com/grafana/grafana/pull/4244), thx @shanielh
+
+### Bugfixes
+* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)
+* **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443)
 
 
-# 4.0.2 (unreleased)
+# 4.0.2 (2016-12-08)
 
 
 ### Enhancements
 ### Enhancements
 * **Playlist**: Add support for kiosk mode [#6727](https://github.com/grafana/grafana/issues/6727)
 * **Playlist**: Add support for kiosk mode [#6727](https://github.com/grafana/grafana/issues/6727)
 
 
 ### Bugfixes
 ### Bugfixes
 * **Alerting**: Add alert message to webhook notifications [#6807](https://github.com/grafana/grafana/issues/6807)
 * **Alerting**: Add alert message to webhook notifications [#6807](https://github.com/grafana/grafana/issues/6807)
+* **Alerting**: Fixes a bug where avg() reducer treated null as zero. [#6879](https://github.com/grafana/grafana/issues/6879)
 * **PNG Rendering**: Fix for server side rendering when using non default http addr bind and domain setting [#6813](https://github.com/grafana/grafana/issues/6813)
 * **PNG Rendering**: Fix for server side rendering when using non default http addr bind and domain setting [#6813](https://github.com/grafana/grafana/issues/6813)
 * **PNG Rendering**: Fix for server side rendering when setting enforce_domain to true [#6769](https://github.com/grafana/grafana/issues/6769)
 * **PNG Rendering**: Fix for server side rendering when setting enforce_domain to true [#6769](https://github.com/grafana/grafana/issues/6769)
 * **Webhooks**: Add content type json to outgoing webhooks [#6822](https://github.com/grafana/grafana/issues/6822)
 * **Webhooks**: Add content type json to outgoing webhooks [#6822](https://github.com/grafana/grafana/issues/6822)

+ 1 - 1
docs/sources/alerting/notifications.md

@@ -91,7 +91,7 @@ Auto resolve incidents | Resolve incidents in pagerduty once the alert goes back
 
 
 # Enable images in notifications {#external-image-store}
 # Enable images in notifications {#external-image-store}
 
 
-Grafan can render the panel associated with the alert rule and include that in the notification. Some types
+Grafana can render the panel associated with the alert rule and include that in the notification. Some types
 of notifications require that this image be publicly accessable (Slack for example). In order to support
 of notifications require that this image be publicly accessable (Slack for example). In order to support
 images in notifications like Slack Grafana can upload the image to an image store. It currently supports
 images in notifications like Slack Grafana can upload the image to an image store. It currently supports
 Amazon S3 for this and Webdav. So to set that up you need to configure the
 Amazon S3 for this and Webdav. So to set that up you need to configure the

+ 2 - 2
docs/sources/http_api/org.md

@@ -93,11 +93,11 @@ parent = "http_api"
 
 
 ## Create Organisation
 ## Create Organisation
 
 
-`POST /api/org`
+`POST /api/orgs`
 
 
 **Example Request**:
 **Example Request**:
 
 
-    POST /api/org HTTP/1.1
+    POST /api/orgs HTTP/1.1
     Accept: application/json
     Accept: application/json
     Content-Type: application/json
     Content-Type: application/json
     Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
     Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk

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

@@ -14,14 +14,14 @@ weight = 1
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-Stable for Debian-based Linux | [4.0.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.1-1480694114_amd64.deb)
+Stable for Debian-based Linux | [4.0.2 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb)
 
 
 ## Install Stable
 ## Install Stable
 
 
 ```
 ```
-$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.1-1480694114_amd64.deb
+$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb
 $ sudo apt-get install -y adduser libfontconfig
 $ sudo apt-get install -y adduser libfontconfig
-$ sudo dpkg -i grafana_4.0.1-1480694114_amd64.deb
+$ sudo dpkg -i grafana_4.0.2-1481203731_amd64.deb
 ```
 ```
 
 
 ## APT Repository
 ## APT Repository

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

@@ -14,24 +14,24 @@ weight = 2
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1-1480694114.x86_64.rpm)
+Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.2 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm)
 
 
 ## Install Stable
 ## Install Stable
 
 
 You can install Grafana using Yum directly.
 You can install Grafana using Yum directly.
 
 
-    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1-1480694114.x86_64.rpm
+    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm
 
 
 Or install manually using `rpm`.
 Or install manually using `rpm`.
 
 
 #### On CentOS / Fedora / Redhat:
 #### On CentOS / Fedora / Redhat:
 
 
     $ sudo yum install initscripts fontconfig
     $ sudo yum install initscripts fontconfig
-    $ sudo rpm -Uvh grafana-4.0.1-1480694114.x86_64.rpm
+    $ sudo rpm -Uvh grafana-4.0.2-1481203731.x86_64.rpm
 
 
 #### On OpenSuse:
 #### On OpenSuse:
 
 
-    $ sudo rpm -i --nodeps grafana-4.0.1-1480694114.x86_64.rpm
+    $ sudo rpm -i --nodeps grafana-4.0.2-1481203731.x86_64.rpm
 
 
 ## Install via YUM Repository
 ## Install via YUM Repository
 
 

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

@@ -13,7 +13,7 @@ weight = 3
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-Latest stable package for Windows | [grafana.4.0.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1.windows-x64.zip)
+Latest stable package for Windows | [grafana.4.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2.windows-x64.zip)
 
 
 ## Configure
 ## Configure
 
 

+ 4 - 4
docs/sources/reference/export_import.md

@@ -9,7 +9,7 @@ weight = 8
 
 
 # Export and Import
 # Export and Import
 
 
-Grafana Dashboads can easily be exported and imported, either from the UI or from the HTTP API.
+Grafana Dashboards can easily be exported and imported, either from the UI or from the HTTP API.
 
 
 ## Exporting a dashboard
 ## Exporting a dashboard
 
 
@@ -22,9 +22,9 @@ The export feature is accessed from the share menu.
 ### Making a dashboard portable
 ### Making a dashboard portable
 
 
 If you want to export a dashboard for others to use then it could be a good idea to
 If you want to export a dashboard for others to use then it could be a good idea to
-add template variables for things like a metric prefix (use contant variable) and server name.
+add template variables for things like a metric prefix (use constant variable) and server name.
 
 
-A template varible of the type `Constant` will automatically be hidden in
+A template variable of the type `Constant` will automatically be hidden in
 the dashboard, and will also be added as an required input when the dashboard is imported.
 the dashboard, and will also be added as an required input when the dashboard is imported.
 
 
 ## Importing a dashboard
 ## Importing a dashboard
@@ -43,7 +43,7 @@ data source you want the dashboard to use and specify any metric prefixes (if th
 
 
 ## Discover dashboards on Grafana.net
 ## Discover dashboards on Grafana.net
 
 
-Find dashboads for common server applications at [Grafana.net/dashboards](https://grafana.net/dashboards).
+Find dashboards for common server applications at [Grafana.net/dashboards](https://grafana.net/dashboards).
 
 
 <img src="/img/docs/v31/gnet_dashboards_list.png">
 <img src="/img/docs/v31/gnet_dashboards_list.png">
 
 

+ 1 - 1
docs/sources/reference/singlestat.md

@@ -1,5 +1,5 @@
 +++
 +++
-title = "Singletat Panel"
+title = "Singlestat Panel"
 keywords = ["grafana", "dashboard", "documentation", "panels", "singlestat"]
 keywords = ["grafana", "dashboard", "documentation", "panels", "singlestat"]
 type = "docs"
 type = "docs"
 [menu.docs]
 [menu.docs]

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
 {
-  "stable": "4.0.1",
-	"testing": "4.0.1"
+  "stable": "4.0.2",
+	"testing": "4.0.2"
 }
 }

+ 2 - 2
packaging/publish/publish.sh

@@ -1,6 +1,6 @@
 #! /usr/bin/env bash
 #! /usr/bin/env bash
-deb_ver=4.0.0-1480439068
-rpm_ver=4.0.0-1480439068
+deb_ver=4.0.2-1481203731
+rpm_ver=4.0.2-1481203731
 
 
 wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
 wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
 
 

+ 4 - 1
pkg/api/api.go

@@ -113,6 +113,9 @@ func Register(r *macaron.Macaron) {
 
 
 			r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
 			r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
 			r.Get("/quotas", wrap(GetUserQuotas))
 			r.Get("/quotas", wrap(GetUserQuotas))
+			r.Put("/helpflags/:id", wrap(SetHelpFlag))
+			// For dev purpose
+			r.Get("/helpflags/clear", wrap(ClearHelpFlags))
 
 
 			r.Get("/preferences", wrap(GetUserPreferences))
 			r.Get("/preferences", wrap(GetUserPreferences))
 			r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
 			r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
@@ -193,7 +196,7 @@ func Register(r *macaron.Macaron) {
 		r.Group("/datasources", func() {
 		r.Group("/datasources", func() {
 			r.Get("/", GetDataSources)
 			r.Get("/", GetDataSources)
 			r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
 			r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
-			r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
+			r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
 			r.Delete("/:id", DeleteDataSource)
 			r.Delete("/:id", DeleteDataSource)
 			r.Get("/:id", wrap(GetDataSourceById))
 			r.Get("/:id", wrap(GetDataSourceById))
 			r.Get("/name/:name", wrap(GetDataSourceByName))
 			r.Get("/name/:name", wrap(GetDataSourceByName))

+ 20 - 0
pkg/api/dashboard.go

@@ -8,6 +8,7 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
@@ -216,9 +217,28 @@ func GetHomeDashboard(c *middleware.Context) Response {
 		return ApiError(500, "Failed to load home dashboard", err)
 		return ApiError(500, "Failed to load home dashboard", err)
 	}
 	}
 
 
+	if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
+		addGettingStartedPanelToHomeDashboard(dash.Dashboard)
+	}
+
 	return Json(200, &dash)
 	return Json(200, &dash)
 }
 }
 
 
+func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
+	rows := dash.Get("rows").MustArray()
+	row := simplejson.NewFromAny(rows[0])
+
+	newpanel := simplejson.NewFromAny(map[string]interface{}{
+		"type": "gettingstarted",
+		"id":   123123,
+		"span": 12,
+	})
+
+	panels := row.Get("panels").MustArray()
+	panels = append(panels, newpanel)
+	row.Set("panels", panels)
+}
+
 func GetDashboardFromJsonFile(c *middleware.Context) {
 func GetDashboardFromJsonFile(c *middleware.Context) {
 	file := c.Params(":file")
 	file := c.Params(":file")
 
 

+ 1 - 74
pkg/api/dataproxy.go

@@ -1,13 +1,9 @@
 package api
 package api
 
 
 import (
 import (
-	"crypto/tls"
-	"crypto/x509"
-	"net"
 	"net/http"
 	"net/http"
 	"net/http/httputil"
 	"net/http/httputil"
 	"net/url"
 	"net/url"
-	"sync"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/api/cloudwatch"
 	"github.com/grafana/grafana/pkg/api/cloudwatch"
@@ -19,75 +15,6 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
-type proxyTransportCache struct {
-	cache map[int64]cachedTransport
-	sync.Mutex
-}
-
-type cachedTransport struct {
-	updated time.Time
-
-	*http.Transport
-}
-
-var ptc = proxyTransportCache{
-	cache: make(map[int64]cachedTransport),
-}
-
-func DataProxyTransport(ds *m.DataSource) (*http.Transport, error) {
-	ptc.Lock()
-	defer ptc.Unlock()
-
-	if t, present := ptc.cache[ds.Id]; present && ds.Updated.Equal(t.updated) {
-		return t.Transport, nil
-	}
-
-	transport := &http.Transport{
-		TLSClientConfig: &tls.Config{
-			InsecureSkipVerify: true,
-		},
-		Proxy: http.ProxyFromEnvironment,
-		Dial: (&net.Dialer{
-			Timeout:   30 * time.Second,
-			KeepAlive: 30 * time.Second,
-		}).Dial,
-		TLSHandshakeTimeout: 10 * time.Second,
-	}
-
-	var tlsAuth, tlsAuthWithCACert bool
-	if ds.JsonData != nil {
-		tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
-		tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
-	}
-
-	if tlsAuth {
-		transport.TLSClientConfig.InsecureSkipVerify = false
-
-		decrypted := ds.SecureJsonData.Decrypt()
-
-		if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
-			caPool := x509.NewCertPool()
-			ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
-			if ok {
-				transport.TLSClientConfig.RootCAs = caPool
-			}
-		}
-
-		cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
-		if err != nil {
-			return nil, err
-		}
-		transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
-	}
-
-	ptc.cache[ds.Id] = cachedTransport{
-		Transport: transport,
-		updated:   ds.Updated,
-	}
-
-	return transport, nil
-}
-
 func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
 func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
 	director := func(req *http.Request) {
 	director := func(req *http.Request) {
 		req.URL.Scheme = targetUrl.Scheme
 		req.URL.Scheme = targetUrl.Scheme
@@ -189,7 +116,7 @@ func ProxyDataSourceRequest(c *middleware.Context) {
 	}
 	}
 
 
 	proxy := NewReverseProxy(ds, proxyPath, targetUrl)
 	proxy := NewReverseProxy(ds, proxyPath, targetUrl)
-	proxy.Transport, err = DataProxyTransport(ds)
+	proxy.Transport, err = ds.GetHttpTransport()
 	if err != nil {
 	if err != nil {
 		c.JsonApiErr(400, "Unable to load TLS certificate", err)
 		c.JsonApiErr(400, "Unable to load TLS certificate", err)
 		return
 		return

+ 1 - 152
pkg/api/dataproxy_test.go

@@ -4,24 +4,18 @@ import (
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"testing"
 	"testing"
-	"time"
 
 
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 
 
-	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/setting"
-	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
 func TestDataSourceProxy(t *testing.T) {
 func TestDataSourceProxy(t *testing.T) {
-
 	Convey("When getting graphite datasource proxy", t, func() {
 	Convey("When getting graphite datasource proxy", t, func() {
-		clearCache()
 		ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
 		ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
 		targetUrl, err := url.Parse(ds.Url)
 		targetUrl, err := url.Parse(ds.Url)
 		proxy := NewReverseProxy(&ds, "/render", targetUrl)
 		proxy := NewReverseProxy(&ds, "/render", targetUrl)
-		proxy.Transport, err = DataProxyTransport(&ds)
+		proxy.Transport, err = ds.GetHttpTransport()
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 
 
 		transport, ok := proxy.Transport.(*http.Transport)
 		transport, ok := proxy.Transport.(*http.Transport)
@@ -40,7 +34,6 @@ func TestDataSourceProxy(t *testing.T) {
 	})
 	})
 
 
 	Convey("When getting influxdb datasource proxy", t, func() {
 	Convey("When getting influxdb datasource proxy", t, func() {
-		clearCache()
 		ds := m.DataSource{
 		ds := m.DataSource{
 			Type:     m.DS_INFLUXDB_08,
 			Type:     m.DS_INFLUXDB_08,
 			Url:      "http://influxdb:8083",
 			Url:      "http://influxdb:8083",
@@ -67,148 +60,4 @@ func TestDataSourceProxy(t *testing.T) {
 			So(queryVals["p"][0], ShouldEqual, "password")
 			So(queryVals["p"][0], ShouldEqual, "password")
 		})
 		})
 	})
 	})
-
-	Convey("When caching a datasource proxy", t, func() {
-		clearCache()
-		ds := m.DataSource{
-			Id:   1,
-			Url:  "http://k8s:8001",
-			Type: "Kubernetes",
-		}
-
-		t1, err := DataProxyTransport(&ds)
-		So(err, ShouldBeNil)
-
-		t2, err := DataProxyTransport(&ds)
-		So(err, ShouldBeNil)
-
-		Convey("Should be using the cached proxy", func() {
-			So(t2, ShouldEqual, t1)
-		})
-	})
-
-	Convey("When getting kubernetes datasource proxy", t, func() {
-		clearCache()
-		setting.SecretKey = "password"
-
-		json := simplejson.New()
-		json.Set("tlsAuth", true)
-		json.Set("tlsAuthWithCACert", true)
-
-		t := time.Now()
-		ds := m.DataSource{
-			Url:     "http://k8s:8001",
-			Type:    "Kubernetes",
-			Updated: t.Add(-2 * time.Minute),
-		}
-
-		transport, err := DataProxyTransport(&ds)
-		So(err, ShouldBeNil)
-
-		Convey("Should have no cert", func() {
-			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
-		})
-
-		ds.JsonData = json
-		ds.SecureJsonData = map[string][]byte{
-			"tlsCACert":     util.Encrypt([]byte(caCert), "password"),
-			"tlsClientCert": util.Encrypt([]byte(clientCert), "password"),
-			"tlsClientKey":  util.Encrypt([]byte(clientKey), "password"),
-		}
-		ds.Updated = t.Add(-1 * time.Minute)
-
-		transport, err = DataProxyTransport(&ds)
-		So(err, ShouldBeNil)
-
-		Convey("Should add cert", func() {
-			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
-			So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
-		})
-
-		ds.JsonData = nil
-		ds.SecureJsonData = map[string][]byte{}
-		ds.Updated = t
-
-		transport, err = DataProxyTransport(&ds)
-		So(err, ShouldBeNil)
-
-		Convey("Should remove cert", func() {
-			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
-			So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
-		})
-	})
-
 }
 }
-
-func clearCache() {
-	ptc.Lock()
-	defer ptc.Unlock()
-
-	ptc.cache = make(map[int64]cachedTransport)
-}
-
-const caCert string = `-----BEGIN CERTIFICATE-----
-MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
-BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda
-MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
-ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg
-QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4
-4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d
-1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN
-iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ
-0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx
-2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5
-VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9
-I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB
-CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7
-adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k
-MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh
-FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
-3lb92xM=
------END CERTIFICATE-----`
-
-const clientCert string = `-----BEGIN CERTIFICATE-----
-MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
-YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
-GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
-ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2
-FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b
-Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo
-GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8
-SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4
-YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF
-AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP
-ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw
-AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q
-4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe
-58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5
-llG/Sw5+FquFuChaA6l5KWy7F3bQyA==
------END CERTIFICATE-----`
-
-const clientKey string = `-----BEGIN RSA PRIVATE KEY-----
-MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV
-u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn
-Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega
-0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI
-LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi
-dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs
-Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk
-CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x
-64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM
-8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh
-WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf
-vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz
-k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs
-DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35
-aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ
-Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo
-jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01
-hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0
-M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8
-v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX
-xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL
-Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0
-Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD
-FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD
-+VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg=
------END RSA PRIVATE KEY-----`

+ 2 - 3
pkg/api/datasources.go

@@ -5,10 +5,9 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
-	"github.com/grafana/grafana/pkg/plugins"
-	//"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
@@ -118,7 +117,7 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp
 		return ApiError(500, "Failed to update datasource", err)
 		return ApiError(500, "Failed to update datasource", err)
 	}
 	}
 
 
-	return Json(200, "Datasource updated")
+	return Json(200, util.DynMap{"message": "Datasource updated"})
 }
 }
 
 
 func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
 func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {

+ 14 - 13
pkg/api/dtos/models.go

@@ -22,19 +22,20 @@ type LoginCommand struct {
 }
 }
 
 
 type CurrentUser struct {
 type CurrentUser struct {
-	IsSignedIn     bool       `json:"isSignedIn"`
-	Id             int64      `json:"id"`
-	Login          string     `json:"login"`
-	Email          string     `json:"email"`
-	Name           string     `json:"name"`
-	LightTheme     bool       `json:"lightTheme"`
-	OrgId          int64      `json:"orgId"`
-	OrgName        string     `json:"orgName"`
-	OrgRole        m.RoleType `json:"orgRole"`
-	IsGrafanaAdmin bool       `json:"isGrafanaAdmin"`
-	GravatarUrl    string     `json:"gravatarUrl"`
-	Timezone       string     `json:"timezone"`
-	Locale         string     `json:"locale"`
+	IsSignedIn     bool         `json:"isSignedIn"`
+	Id             int64        `json:"id"`
+	Login          string       `json:"login"`
+	Email          string       `json:"email"`
+	Name           string       `json:"name"`
+	LightTheme     bool         `json:"lightTheme"`
+	OrgId          int64        `json:"orgId"`
+	OrgName        string       `json:"orgName"`
+	OrgRole        m.RoleType   `json:"orgRole"`
+	IsGrafanaAdmin bool         `json:"isGrafanaAdmin"`
+	GravatarUrl    string       `json:"gravatarUrl"`
+	Timezone       string       `json:"timezone"`
+	Locale         string       `json:"locale"`
+	HelpFlags1     m.HelpFlags1 `json:"helpFlags1"`
 }
 }
 
 
 type DashboardMeta struct {
 type DashboardMeta struct {

+ 7 - 6
pkg/api/frontendsettings.go

@@ -122,12 +122,13 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 	panels := map[string]interface{}{}
 	panels := map[string]interface{}{}
 	for _, panel := range enabledPlugins.Panels {
 	for _, panel := range enabledPlugins.Panels {
 		panels[panel.Id] = map[string]interface{}{
 		panels[panel.Id] = map[string]interface{}{
-			"module":  panel.Module,
-			"baseUrl": panel.BaseUrl,
-			"name":    panel.Name,
-			"id":      panel.Id,
-			"info":    panel.Info,
-			"sort":    getPanelSort(panel.Id),
+			"module":       panel.Module,
+			"baseUrl":      panel.BaseUrl,
+			"name":         panel.Name,
+			"id":           panel.Id,
+			"info":         panel.Info,
+			"hideFromList": panel.HideFromList,
+			"sort":         getPanelSort(panel.Id),
 		}
 		}
 	}
 	}
 
 

+ 1 - 0
pkg/api/index.go

@@ -58,6 +58,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 			LightTheme:     prefs.Theme == "light",
 			LightTheme:     prefs.Theme == "light",
 			Timezone:       prefs.Timezone,
 			Timezone:       prefs.Timezone,
 			Locale:         locale,
 			Locale:         locale,
+			HelpFlags1:     c.HelpFlags1,
 		},
 		},
 		Settings:                settings,
 		Settings:                settings,
 		AppUrl:                  appUrl,
 		AppUrl:                  appUrl,

+ 4 - 3
pkg/api/metrics.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb/testdata"
 	"github.com/grafana/grafana/pkg/tsdb/testdata"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
@@ -25,9 +26,9 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
 			MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
 			MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
 			IntervalMs:    query.Get("intervalMs").MustInt64(1000),
 			IntervalMs:    query.Get("intervalMs").MustInt64(1000),
 			Model:         query,
 			Model:         query,
-			DataSource: &tsdb.DataSourceInfo{
-				Name:     "Grafana TestDataDB",
-				PluginId: "grafana-testdata-datasource",
+			DataSource: &models.DataSource{
+				Name: "Grafana TestDataDB",
+				Type: "grafana-testdata-datasource",
 			},
 			},
 		})
 		})
 	}
 	}

+ 31 - 0
pkg/api/user.go

@@ -180,3 +180,34 @@ func SearchUsers(c *middleware.Context) Response {
 
 
 	return Json(200, query.Result)
 	return Json(200, query.Result)
 }
 }
+
+func SetHelpFlag(c *middleware.Context) Response {
+	flag := c.ParamsInt64(":id")
+
+	bitmask := &c.HelpFlags1
+	bitmask.AddFlag(m.HelpFlags1(flag))
+
+	cmd := m.SetUserHelpFlagCommand{
+		UserId:     c.UserId,
+		HelpFlags1: *bitmask,
+	}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed to update help flag", err)
+	}
+
+	return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
+}
+
+func ClearHelpFlags(c *middleware.Context) Response {
+	cmd := m.SetUserHelpFlagCommand{
+		UserId:     c.UserId,
+		HelpFlags1: m.HelpFlags1(0),
+	}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed to update help flag", err)
+	}
+
+	return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
+}

+ 2 - 0
pkg/metrics/metrics.go

@@ -46,6 +46,7 @@ var (
 	M_Alerting_Notification_Sent_Webhook   Counter
 	M_Alerting_Notification_Sent_Webhook   Counter
 	M_Alerting_Notification_Sent_PagerDuty Counter
 	M_Alerting_Notification_Sent_PagerDuty Counter
 	M_Alerting_Notification_Sent_Victorops Counter
 	M_Alerting_Notification_Sent_Victorops Counter
+	M_Alerting_Notification_Sent_OpsGenie  Counter
 
 
 	// Timers
 	// Timers
 	M_DataSource_ProxyReq_Timer Timer
 	M_DataSource_ProxyReq_Timer Timer
@@ -110,6 +111,7 @@ func initMetricVars(settings *MetricSettings) {
 	M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook")
 	M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook")
 	M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
 	M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
 	M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
 	M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
+	M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
 
 
 	// Timers
 	// Timers
 	M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
 	M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")

+ 4 - 0
pkg/middleware/middleware.go

@@ -229,6 +229,10 @@ func (ctx *Context) HasUserRole(role m.RoleType) bool {
 	return ctx.OrgRole.Includes(role)
 	return ctx.OrgRole.Includes(role)
 }
 }
 
 
+func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool {
+	return ctx.HelpFlags1.HasFlag(flag)
+}
+
 func (ctx *Context) TimeRequest(timer metrics.Timer) {
 func (ctx *Context) TimeRequest(timer metrics.Timer) {
 	ctx.Data["perfmon.timer"] = timer
 	ctx.Data["perfmon.timer"] = timer
 }
 }

+ 95 - 0
pkg/models/datasource_cache.go

@@ -0,0 +1,95 @@
+package models
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"net"
+	"net/http"
+	"sync"
+	"time"
+)
+
+type proxyTransportCache struct {
+	cache map[int64]cachedTransport
+	sync.Mutex
+}
+
+type cachedTransport struct {
+	updated time.Time
+
+	*http.Transport
+}
+
+var ptc = proxyTransportCache{
+	cache: make(map[int64]cachedTransport),
+}
+
+func (ds *DataSource) GetHttpClient() (*http.Client, error) {
+	transport, err := ds.GetHttpTransport()
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &http.Client{
+		Timeout:   time.Duration(30 * time.Second),
+		Transport: transport,
+	}, nil
+}
+
+func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
+	ptc.Lock()
+	defer ptc.Unlock()
+
+	if t, present := ptc.cache[ds.Id]; present && ds.Updated.Equal(t.updated) {
+		return t.Transport, nil
+	}
+
+	transport := &http.Transport{
+		TLSClientConfig: &tls.Config{
+			InsecureSkipVerify: true,
+		},
+		Proxy: http.ProxyFromEnvironment,
+		Dial: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: 30 * time.Second,
+		}).Dial,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+	}
+
+	var tlsAuth, tlsAuthWithCACert bool
+	if ds.JsonData != nil {
+		tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
+		tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
+	}
+
+	if tlsAuth {
+		transport.TLSClientConfig.InsecureSkipVerify = false
+
+		decrypted := ds.SecureJsonData.Decrypt()
+
+		if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
+			caPool := x509.NewCertPool()
+			ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
+			if ok {
+				transport.TLSClientConfig.RootCAs = caPool
+			}
+		}
+
+		cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
+		if err != nil {
+			return nil, err
+		}
+		transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
+	}
+
+	ptc.cache[ds.Id] = cachedTransport{
+		Transport: transport,
+		updated:   ds.Updated,
+	}
+
+	return transport, nil
+}

+ 157 - 0
pkg/models/datasource_cache_test.go

@@ -0,0 +1,157 @@
+package models
+
+import (
+	"testing"
+	"time"
+
+	. "github.com/smartystreets/goconvey/convey"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+func TestDataSourceCache(t *testing.T) {
+	Convey("When caching a datasource proxy", t, func() {
+		clearCache()
+		ds := DataSource{
+			Id:   1,
+			Url:  "http://k8s:8001",
+			Type: "Kubernetes",
+		}
+
+		t1, err := ds.GetHttpTransport()
+		So(err, ShouldBeNil)
+
+		t2, err := ds.GetHttpTransport()
+		So(err, ShouldBeNil)
+
+		Convey("Should be using the cached proxy", func() {
+			So(t2, ShouldEqual, t1)
+		})
+	})
+
+	Convey("When getting kubernetes datasource proxy", t, func() {
+		clearCache()
+		setting.SecretKey = "password"
+
+		json := simplejson.New()
+		json.Set("tlsAuth", true)
+		json.Set("tlsAuthWithCACert", true)
+
+		t := time.Now()
+		ds := DataSource{
+			Url:     "http://k8s:8001",
+			Type:    "Kubernetes",
+			Updated: t.Add(-2 * time.Minute),
+		}
+
+		transport, err := ds.GetHttpTransport()
+		So(err, ShouldBeNil)
+
+		Convey("Should have no cert", func() {
+			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
+		})
+
+		ds.JsonData = json
+		ds.SecureJsonData = map[string][]byte{
+			"tlsCACert":     util.Encrypt([]byte(caCert), "password"),
+			"tlsClientCert": util.Encrypt([]byte(clientCert), "password"),
+			"tlsClientKey":  util.Encrypt([]byte(clientKey), "password"),
+		}
+		ds.Updated = t.Add(-1 * time.Minute)
+
+		transport, err = ds.GetHttpTransport()
+		So(err, ShouldBeNil)
+
+		Convey("Should add cert", func() {
+			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
+			So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
+		})
+
+		ds.JsonData = nil
+		ds.SecureJsonData = map[string][]byte{}
+		ds.Updated = t
+
+		transport, err = ds.GetHttpTransport()
+		So(err, ShouldBeNil)
+
+		Convey("Should remove cert", func() {
+			So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
+			So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
+		})
+	})
+}
+
+func clearCache() {
+	ptc.Lock()
+	defer ptc.Unlock()
+
+	ptc.cache = make(map[int64]cachedTransport)
+}
+
+const caCert string = `-----BEGIN CERTIFICATE-----
+MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
+BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda
+MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg
+QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4
+4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d
+1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN
+iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ
+0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx
+2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5
+VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9
+I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB
+CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7
+adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k
+MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh
+FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
+3lb92xM=
+-----END CERTIFICATE-----`
+
+const clientCert string = `-----BEGIN CERTIFICATE-----
+MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
+YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
+GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2
+FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b
+Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo
+GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8
+SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4
+YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP
+ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw
+AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q
+4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe
+58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5
+llG/Sw5+FquFuChaA6l5KWy7F3bQyA==
+-----END CERTIFICATE-----`
+
+const clientKey string = `-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV
+u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn
+Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega
+0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI
+LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi
+dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs
+Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk
+CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x
+64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM
+8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh
+WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf
+vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz
+k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs
+DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35
+aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ
+Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo
+jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01
+hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0
+M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8
+v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX
+xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL
+Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0
+Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD
+FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD
++VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg=
+-----END RSA PRIVATE KEY-----`

+ 18 - 0
pkg/models/helpflags.go

@@ -0,0 +1,18 @@
+package models
+
+type HelpFlags1 uint64
+
+const (
+	HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota
+	HelpFlagDashboardHelp1
+)
+
+func (f HelpFlags1) HasFlag(flag HelpFlags1) bool { return f&flag != 0 }
+func (f *HelpFlags1) AddFlag(flag HelpFlags1)     { *f |= flag }
+func (f *HelpFlags1) ClearFlag(flag HelpFlags1)   { *f &= ^flag }
+func (f *HelpFlags1) ToggleFlag(flag HelpFlags1)  { *f ^= flag }
+
+type SetUserHelpFlagCommand struct {
+	HelpFlags1 HelpFlags1
+	UserId     int64
+}

+ 2 - 0
pkg/models/user.go

@@ -22,6 +22,7 @@ type User struct {
 	Company       string
 	Company       string
 	EmailVerified bool
 	EmailVerified bool
 	Theme         string
 	Theme         string
+	HelpFlags1    HelpFlags1
 
 
 	IsAdmin bool
 	IsAdmin bool
 	OrgId   int64
 	OrgId   int64
@@ -144,6 +145,7 @@ type SignedInUser struct {
 	Email          string
 	Email          string
 	ApiKeyId       int64
 	ApiKeyId       int64
 	IsGrafanaAdmin bool
 	IsGrafanaAdmin bool
+	HelpFlags1     HelpFlags1
 }
 }
 
 
 type UserProfileDTO struct {
 type UserProfileDTO struct {

+ 1 - 0
pkg/plugins/models.go

@@ -38,6 +38,7 @@ type PluginBase struct {
 	Includes     []*PluginInclude   `json:"includes"`
 	Includes     []*PluginInclude   `json:"includes"`
 	Module       string             `json:"module"`
 	Module       string             `json:"module"`
 	BaseUrl      string             `json:"baseUrl"`
 	BaseUrl      string             `json:"baseUrl"`
+	HideFromList bool               `json:"hideFromList"`
 
 
 	IncludedInAppId string `json:"-"`
 	IncludedInAppId string `json:"-"`
 	PluginDir       string `json:"-"`
 	PluginDir       string `json:"-"`

+ 3 - 15
pkg/services/alerting/conditions/query.go

@@ -119,21 +119,9 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRa
 		TimeRange: timeRange,
 		TimeRange: timeRange,
 		Queries: []*tsdb.Query{
 		Queries: []*tsdb.Query{
 			{
 			{
-				RefId: "A",
-				Model: c.Query.Model,
-				DataSource: &tsdb.DataSourceInfo{
-					Id:                datasource.Id,
-					Name:              datasource.Name,
-					PluginId:          datasource.Type,
-					Url:               datasource.Url,
-					User:              datasource.User,
-					Password:          datasource.Password,
-					Database:          datasource.Database,
-					BasicAuth:         datasource.BasicAuth,
-					BasicAuthUser:     datasource.BasicAuthUser,
-					BasicAuthPassword: datasource.BasicAuthPassword,
-					JsonData:          datasource.JsonData,
-				},
+				RefId:      "A",
+				Model:      c.Query.Model,
+				DataSource: datasource,
 			},
 			},
 		},
 		},
 	}
 	}

+ 19 - 19
pkg/services/alerting/conditions/reducer_test.go

@@ -11,25 +11,6 @@ import (
 
 
 func TestSimpleReducer(t *testing.T) {
 func TestSimpleReducer(t *testing.T) {
 	Convey("Test simple reducer by calculating", t, func() {
 	Convey("Test simple reducer by calculating", t, func() {
-		Convey("avg", func() {
-			result := testReducer("avg", 1, 2, 3)
-			So(result, ShouldEqual, float64(2))
-		})
-
-		Convey("avg of none null data", func() {
-			reducer := NewSimpleReducer("avg")
-			series := &tsdb.TimeSeries{
-				Name: "test time serie",
-			}
-
-			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 1))
-			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
-			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
-			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4))
-
-			So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
-		})
-
 		Convey("sum", func() {
 		Convey("sum", func() {
 			result := testReducer("sum", 1, 2, 3)
 			result := testReducer("sum", 1, 2, 3)
 			So(result, ShouldEqual, float64(6))
 			So(result, ShouldEqual, float64(6))
@@ -69,6 +50,25 @@ func TestSimpleReducer(t *testing.T) {
 			result := testReducer("median", 1)
 			result := testReducer("median", 1)
 			So(result, ShouldEqual, float64(1))
 			So(result, ShouldEqual, float64(1))
 		})
 		})
+
+		Convey("avg", func() {
+			result := testReducer("avg", 1, 2, 3)
+			So(result, ShouldEqual, float64(2))
+		})
+
+		Convey("avg of number values and null values should ignore nulls", func() {
+			reducer := NewSimpleReducer("avg")
+			series := &tsdb.TimeSeries{
+				Name: "test time serie",
+			}
+
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 1))
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4))
+
+			So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
+		})
 	})
 	})
 }
 }
 
 

+ 118 - 0
pkg/services/alerting/notifiers/opsgenie.go

@@ -0,0 +1,118 @@
+package notifiers
+
+import (
+	"fmt"
+	"strconv"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
+)
+
+func init() {
+	alerting.RegisterNotifier("opsgenie", NewOpsGenieNotifier)
+}
+
+var (
+	opsgenieCreateAlertURL string = "https://api.opsgenie.com/v1/json/alert"
+	opsgenieCloseAlertURL  string = "https://api.opsgenie.com/v1/json/alert/close"
+)
+
+func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+	autoClose := model.Settings.Get("autoClose").MustBool(true)
+	apiKey := model.Settings.Get("apiKey").MustString()
+	if apiKey == "" {
+		return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"}
+	}
+
+	return &OpsGenieNotifier{
+		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+		ApiKey:       apiKey,
+		AutoClose:    autoClose,
+		log:          log.New("alerting.notifier.opsgenie"),
+	}, nil
+}
+
+type OpsGenieNotifier struct {
+	NotifierBase
+	ApiKey    string
+	AutoClose bool
+	log       log.Logger
+}
+
+func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
+	metrics.M_Alerting_Notification_Sent_OpsGenie.Inc(1)
+
+	var err error
+	switch evalContext.Rule.State {
+	case m.AlertStateOK:
+		if this.AutoClose {
+			err = this.closeAlert(evalContext)
+		}
+	case m.AlertStateAlerting:
+		err = this.createAlert(evalContext)
+	}
+	return err
+}
+
+func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error {
+	this.log.Info("Creating OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+
+	ruleUrl, err := evalContext.GetRuleUrl()
+	if err != nil {
+		this.log.Error("Failed get rule link", "error", err)
+		return err
+	}
+
+	bodyJSON := simplejson.New()
+	bodyJSON.Set("apiKey", this.ApiKey)
+	bodyJSON.Set("message", evalContext.Rule.Name)
+	bodyJSON.Set("source", "Grafana")
+	bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
+	bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message))
+
+	details := simplejson.New()
+	details.Set("url", ruleUrl)
+	if evalContext.ImagePublicUrl != "" {
+		details.Set("image", evalContext.ImagePublicUrl)
+	}
+
+	bodyJSON.Set("details", details)
+	body, _ := bodyJSON.MarshalJSON()
+
+	cmd := &m.SendWebhookSync{
+		Url:        opsgenieCreateAlertURL,
+		Body:       string(body),
+		HttpMethod: "POST",
+	}
+
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
+	}
+
+	return nil
+}
+
+func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error {
+	this.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+
+	bodyJSON := simplejson.New()
+	bodyJSON.Set("apiKey", this.ApiKey)
+	bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
+	body, _ := bodyJSON.MarshalJSON()
+
+	cmd := &m.SendWebhookSync{
+		Url:        opsgenieCloseAlertURL,
+		Body:       string(body),
+		HttpMethod: "POST",
+	}
+
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
+	}
+
+	return nil
+}

+ 52 - 0
pkg/services/alerting/notifiers/opsgenie_test.go

@@ -0,0 +1,52 @@
+package notifiers
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	m "github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestOpsGenieNotifier(t *testing.T) {
+	Convey("OpsGenie notifier tests", t, func() {
+
+		Convey("Parsing alert notification from settings", func() {
+			Convey("empty settings should return error", func() {
+				json := `{ }`
+
+				settingsJSON, _ := simplejson.NewJson([]byte(json))
+				model := &m.AlertNotification{
+					Name:     "opsgenie_testing",
+					Type:     "opsgenie",
+					Settings: settingsJSON,
+				}
+
+				_, err := NewOpsGenieNotifier(model)
+				So(err, ShouldNotBeNil)
+			})
+
+			Convey("settings should trigger incident", func() {
+				json := `
+				{
+          "apiKey": "abcdefgh0123456789"
+				}`
+
+				settingsJSON, _ := simplejson.NewJson([]byte(json))
+				model := &m.AlertNotification{
+					Name:     "opsgenie_testing",
+					Type:     "opsgenie",
+					Settings: settingsJSON,
+				}
+
+				not, err := NewOpsGenieNotifier(model)
+				opsgenieNotifier := not.(*OpsGenieNotifier)
+
+				So(err, ShouldBeNil)
+				So(opsgenieNotifier.Name, ShouldEqual, "opsgenie_testing")
+				So(opsgenieNotifier.Type, ShouldEqual, "opsgenie")
+				So(opsgenieNotifier.ApiKey, ShouldEqual, "abcdefgh0123456789")
+			})
+		})
+	})
+}

+ 4 - 0
pkg/services/sqlstore/migrations/user_mig.go

@@ -88,4 +88,8 @@ func addUserMigrations(mg *Migrator) {
 	}))
 	}))
 
 
 	mg.AddMigration("Drop old table user_v1", NewDropTableMigration("user_v1"))
 	mg.AddMigration("Drop old table user_v1", NewDropTableMigration("user_v1"))
+
+	mg.AddMigration("Add column help_flags1 to user table", NewAddColumnMigration(userV2, &Column{
+		Name: "help_flags1", Type: DB_BigInt, Nullable: false, Default: "0",
+	}))
 }
 }

+ 20 - 1
pkg/services/sqlstore/user.go

@@ -28,6 +28,7 @@ func init() {
 	bus.AddHandler("sql", DeleteUser)
 	bus.AddHandler("sql", DeleteUser)
 	bus.AddHandler("sql", SetUsingOrg)
 	bus.AddHandler("sql", SetUsingOrg)
 	bus.AddHandler("sql", UpdateUserPermissions)
 	bus.AddHandler("sql", UpdateUserPermissions)
+	bus.AddHandler("sql", SetUserHelpFlag)
 }
 }
 
 
 func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *session) (int64, error) {
 func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *session) (int64, error) {
@@ -207,7 +208,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	} else if has == false {
 	} else if has == false {
-		return  m.ErrUserNotFound
+		return m.ErrUserNotFound
 	}
 	}
 
 
 	query.Result = user
 	query.Result = user
@@ -308,6 +309,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
 	                u.email        as email,
 	                u.email        as email,
 	                u.login        as login,
 	                u.login        as login,
 									u.name         as name,
 									u.name         as name,
+									u.help_flags1  as help_flags1,
 	                org.name       as org_name,
 	                org.name       as org_name,
 	                org_user.role  as org_role,
 	                org_user.role  as org_role,
 	                org.id         as org_id
 	                org.id         as org_id
@@ -380,3 +382,20 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error {
 		return err
 		return err
 	})
 	})
 }
 }
+
+func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
+	return inTransaction2(func(sess *session) error {
+
+		user := m.User{
+			Id:         cmd.UserId,
+			HelpFlags1: cmd.HelpFlags1,
+			Updated:    time.Now(),
+		}
+
+		if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
+			return err
+		}
+
+		return nil
+	})
+}

+ 4 - 7
pkg/tsdb/batch.go

@@ -1,9 +1,6 @@
 package tsdb
 package tsdb
 
 
-import (
-	"context"
-	"errors"
-)
+import "context"
 
 
 type Batch struct {
 type Batch struct {
 	DataSourceId int64
 	DataSourceId int64
@@ -24,12 +21,12 @@ func newBatch(dsId int64, queries QuerySlice) *Batch {
 }
 }
 
 
 func (bg *Batch) process(ctx context.Context, queryContext *QueryContext) {
 func (bg *Batch) process(ctx context.Context, queryContext *QueryContext) {
-	executor := getExecutorFor(bg.Queries[0].DataSource)
+	executor, err := getExecutorFor(bg.Queries[0].DataSource)
 
 
-	if executor == nil {
+	if err != nil {
 		bg.Done = true
 		bg.Done = true
 		result := &BatchResult{
 		result := &BatchResult{
-			Error:        errors.New("Could not find executor for data source type: " + bg.Queries[0].DataSource.PluginId),
+			Error:        err,
 			QueryResults: make(map[string]*QueryResult),
 			QueryResults: make(map[string]*QueryResult),
 		}
 		}
 		for _, query := range bg.Queries {
 		for _, query := range bg.Queries {

+ 16 - 6
pkg/tsdb/executor.go

@@ -1,6 +1,11 @@
 package tsdb
 package tsdb
 
 
-import "context"
+import (
+	"context"
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/models"
+)
 
 
 type Executor interface {
 type Executor interface {
 	Execute(ctx context.Context, queries QuerySlice, query *QueryContext) *BatchResult
 	Execute(ctx context.Context, queries QuerySlice, query *QueryContext) *BatchResult
@@ -8,17 +13,22 @@ type Executor interface {
 
 
 var registry map[string]GetExecutorFn
 var registry map[string]GetExecutorFn
 
 
-type GetExecutorFn func(dsInfo *DataSourceInfo) Executor
+type GetExecutorFn func(dsInfo *models.DataSource) (Executor, error)
 
 
 func init() {
 func init() {
 	registry = make(map[string]GetExecutorFn)
 	registry = make(map[string]GetExecutorFn)
 }
 }
 
 
-func getExecutorFor(dsInfo *DataSourceInfo) Executor {
-	if fn, exists := registry[dsInfo.PluginId]; exists {
-		return fn(dsInfo)
+func getExecutorFor(dsInfo *models.DataSource) (Executor, error) {
+	if fn, exists := registry[dsInfo.Type]; exists {
+		executor, err := fn(dsInfo)
+		if err != nil {
+			return nil, err
+		}
+
+		return executor, nil
 	}
 	}
-	return nil
+	return nil, fmt.Errorf("Could not find executor for data source type: %s", dsInfo.Type)
 }
 }
 
 
 func RegisterExecutor(pluginId string, fn GetExecutorFn) {
 func RegisterExecutor(pluginId string, fn GetExecutorFn) {

+ 7 - 3
pkg/tsdb/fake_test.go

@@ -1,6 +1,10 @@
 package tsdb
 package tsdb
 
 
-import "context"
+import (
+	"context"
+
+	"github.com/grafana/grafana/pkg/models"
+)
 
 
 type FakeExecutor struct {
 type FakeExecutor struct {
 	results   map[string]*QueryResult
 	results   map[string]*QueryResult
@@ -9,11 +13,11 @@ type FakeExecutor struct {
 
 
 type ResultsFn func(context *QueryContext) *QueryResult
 type ResultsFn func(context *QueryContext) *QueryResult
 
 
-func NewFakeExecutor(dsInfo *DataSourceInfo) *FakeExecutor {
+func NewFakeExecutor(dsInfo *models.DataSource) (*FakeExecutor, error) {
 	return &FakeExecutor{
 	return &FakeExecutor{
 		results:   make(map[string]*QueryResult),
 		results:   make(map[string]*QueryResult),
 		resultsFn: make(map[string]ResultsFn),
 		resultsFn: make(map[string]ResultsFn),
-	}
+	}, nil
 }
 }
 
 
 func (e *FakeExecutor) Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult {
 func (e *FakeExecutor) Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult {

+ 16 - 8
pkg/tsdb/graphite/graphite.go

@@ -14,28 +14,36 @@ import (
 	"golang.org/x/net/context/ctxhttp"
 	"golang.org/x/net/context/ctxhttp"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 )
 
 
 type GraphiteExecutor struct {
 type GraphiteExecutor struct {
-	*tsdb.DataSourceInfo
+	*models.DataSource
+	HttpClient *http.Client
 }
 }
 
 
-func NewGraphiteExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
-	return &GraphiteExecutor{dsInfo}
+func NewGraphiteExecutor(datasource *models.DataSource) (tsdb.Executor, error) {
+	httpClient, err := datasource.GetHttpClient()
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &GraphiteExecutor{
+		DataSource: datasource,
+		HttpClient: httpClient,
+	}, nil
 }
 }
 
 
 var (
 var (
-	glog       log.Logger
-	HttpClient *http.Client
+	glog log.Logger
 )
 )
 
 
 func init() {
 func init() {
 	glog = log.New("tsdb.graphite")
 	glog = log.New("tsdb.graphite")
 	tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
 	tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
-
-	HttpClient = tsdb.GetDefaultClient()
 }
 }
 
 
 func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
 func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
@@ -66,7 +74,7 @@ func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
 		return result
 		return result
 	}
 	}
 
 
-	res, err := ctxhttp.Do(ctx, HttpClient, req)
+	res, err := ctxhttp.Do(ctx, e.HttpClient, req)
 	if err != nil {
 	if err != nil {
 		result.Error = err
 		result.Error = err
 		return result
 		return result

+ 0 - 29
pkg/tsdb/http.go

@@ -1,29 +0,0 @@
-package tsdb
-
-import (
-	"crypto/tls"
-	"net"
-	"net/http"
-	"time"
-)
-
-func GetDefaultClient() *http.Client {
-	tr := &http.Transport{
-		Proxy: http.ProxyFromEnvironment,
-		DialContext: (&net.Dialer{
-			Timeout:   30 * time.Second,
-			KeepAlive: 30 * time.Second,
-		}).DialContext,
-		MaxIdleConns:          100,
-		IdleConnTimeout:       90 * time.Second,
-		TLSHandshakeTimeout:   10 * time.Second,
-		ExpectContinueTimeout: 1 * time.Second,
-
-		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-	}
-
-	return &http.Client{
-		Timeout:   time.Duration(30 * time.Second),
-		Transport: tr,
-	}
-}

+ 16 - 10
pkg/tsdb/influxdb/influxdb.go

@@ -11,34 +11,40 @@ import (
 	"golang.org/x/net/context/ctxhttp"
 	"golang.org/x/net/context/ctxhttp"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 )
 
 
 type InfluxDBExecutor struct {
 type InfluxDBExecutor struct {
-	*tsdb.DataSourceInfo
+	*models.DataSource
 	QueryParser    *InfluxdbQueryParser
 	QueryParser    *InfluxdbQueryParser
 	ResponseParser *ResponseParser
 	ResponseParser *ResponseParser
+	HttpClient     *http.Client
 }
 }
 
 
-func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
+func NewInfluxDBExecutor(datasource *models.DataSource) (tsdb.Executor, error) {
+	httpClient, err := datasource.GetHttpClient()
+
+	if err != nil {
+		return nil, err
+	}
+
 	return &InfluxDBExecutor{
 	return &InfluxDBExecutor{
-		DataSourceInfo: dsInfo,
+		DataSource:     datasource,
 		QueryParser:    &InfluxdbQueryParser{},
 		QueryParser:    &InfluxdbQueryParser{},
 		ResponseParser: &ResponseParser{},
 		ResponseParser: &ResponseParser{},
-	}
+		HttpClient:     httpClient,
+	}, nil
 }
 }
 
 
 var (
 var (
-	glog       log.Logger
-	HttpClient *http.Client
+	glog log.Logger
 )
 )
 
 
 func init() {
 func init() {
 	glog = log.New("tsdb.influxdb")
 	glog = log.New("tsdb.influxdb")
 	tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor)
 	tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor)
-
-	HttpClient = tsdb.GetDefaultClient()
 }
 }
 
 
 func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
 func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
@@ -63,7 +69,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
 		return result.WithError(err)
 		return result.WithError(err)
 	}
 	}
 
 
-	resp, err := ctxhttp.Do(ctx, HttpClient, req)
+	resp, err := ctxhttp.Do(ctx, e.HttpClient, req)
 	if err != nil {
 	if err != nil {
 		return result.WithError(err)
 		return result.WithError(err)
 	}
 	}
@@ -95,7 +101,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
 func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.QueryContext) (*Query, error) {
 func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.QueryContext) (*Query, error) {
 	for _, v := range queries {
 	for _, v := range queries {
 
 
-		query, err := e.QueryParser.Parse(v.Model, e.DataSourceInfo)
+		query, err := e.QueryParser.Parse(v.Model, e.DataSource)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}

+ 2 - 2
pkg/tsdb/influxdb/model_parser.go

@@ -4,12 +4,12 @@ import (
 	"strconv"
 	"strconv"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/tsdb"
+	"github.com/grafana/grafana/pkg/models"
 )
 )
 
 
 type InfluxdbQueryParser struct{}
 type InfluxdbQueryParser struct{}
 
 
-func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) {
+func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.DataSource) (*Query, error) {
 	policy := model.Get("policy").MustString("default")
 	policy := model.Get("policy").MustString("default")
 	rawQuery := model.Get("query").MustString("")
 	rawQuery := model.Get("query").MustString("")
 	useRawQuery := model.Get("rawQuery").MustBool(false)
 	useRawQuery := model.Get("rawQuery").MustBool(false)

+ 2 - 2
pkg/tsdb/influxdb/model_parser_test.go

@@ -4,7 +4,7 @@ import (
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/tsdb"
+	"github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -12,7 +12,7 @@ func TestInfluxdbQueryParser(t *testing.T) {
 	Convey("Influxdb query parser", t, func() {
 	Convey("Influxdb query parser", t, func() {
 
 
 		parser := &InfluxdbQueryParser{}
 		parser := &InfluxdbQueryParser{}
-		dsInfo := &tsdb.DataSourceInfo{
+		dsInfo := &models.DataSource{
 			JsonData: simplejson.New(),
 			JsonData: simplejson.New(),
 		}
 		}
 
 

+ 2 - 15
pkg/tsdb/models.go

@@ -2,6 +2,7 @@ package tsdb
 
 
 import (
 import (
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
 	"gopkg.in/guregu/null.v3"
 	"gopkg.in/guregu/null.v3"
 )
 )
 
 
@@ -9,7 +10,7 @@ type Query struct {
 	RefId         string
 	RefId         string
 	Model         *simplejson.Json
 	Model         *simplejson.Json
 	Depends       []string
 	Depends       []string
-	DataSource    *DataSourceInfo
+	DataSource    *models.DataSource
 	Results       []*TimeSeries
 	Results       []*TimeSeries
 	Exclude       bool
 	Exclude       bool
 	MaxDataPoints int64
 	MaxDataPoints int64
@@ -28,20 +29,6 @@ type Response struct {
 	Results      map[string]*QueryResult `json:"results"`
 	Results      map[string]*QueryResult `json:"results"`
 }
 }
 
 
-type DataSourceInfo struct {
-	Id                int64
-	Name              string
-	PluginId          string
-	Url               string
-	Password          string
-	User              string
-	Database          string
-	BasicAuth         bool
-	BasicAuthUser     string
-	BasicAuthPassword string
-	JsonData          *simplejson.Json
-}
-
 type BatchTiming struct {
 type BatchTiming struct {
 	TimeElapsed int64
 	TimeElapsed int64
 }
 }

+ 16 - 8
pkg/tsdb/opentsdb/opentsdb.go

@@ -17,28 +17,36 @@ import (
 	"gopkg.in/guregu/null.v3"
 	"gopkg.in/guregu/null.v3"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 )
 
 
 type OpenTsdbExecutor struct {
 type OpenTsdbExecutor struct {
-	*tsdb.DataSourceInfo
+	*models.DataSource
+	httpClient *http.Client
 }
 }
 
 
-func NewOpenTsdbExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
-	return &OpenTsdbExecutor{dsInfo}
+func NewOpenTsdbExecutor(datasource *models.DataSource) (tsdb.Executor, error) {
+	httpClient, err := datasource.GetHttpClient()
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &OpenTsdbExecutor{
+		DataSource: datasource,
+		httpClient: httpClient,
+	}, nil
 }
 }
 
 
 var (
 var (
-	plog       log.Logger
-	HttpClient *http.Client
+	plog log.Logger
 )
 )
 
 
 func init() {
 func init() {
 	plog = log.New("tsdb.opentsdb")
 	plog = log.New("tsdb.opentsdb")
 	tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor)
 	tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor)
-
-	HttpClient = tsdb.GetDefaultClient()
 }
 }
 
 
 func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
 func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
@@ -64,7 +72,7 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
 		return result
 		return result
 	}
 	}
 
 
-	res, err := ctxhttp.Do(ctx, HttpClient, req)
+	res, err := ctxhttp.Do(ctx, e.httpClient, req)
 	if err != nil {
 	if err != nil {
 		result.Error = err
 		result.Error = err
 		return result
 		return result

+ 17 - 4
pkg/tsdb/prometheus/prometheus.go

@@ -9,18 +9,30 @@ import (
 
 
 	"gopkg.in/guregu/null.v3"
 	"gopkg.in/guregu/null.v3"
 
 
+	"net/http"
+
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/prometheus/client_golang/api/prometheus"
 	"github.com/prometheus/client_golang/api/prometheus"
 	pmodel "github.com/prometheus/common/model"
 	pmodel "github.com/prometheus/common/model"
 )
 )
 
 
 type PrometheusExecutor struct {
 type PrometheusExecutor struct {
-	*tsdb.DataSourceInfo
+	*models.DataSource
+	Transport *http.Transport
 }
 }
 
 
-func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
-	return &PrometheusExecutor{dsInfo}
+func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) {
+	transport, err := dsInfo.GetHttpTransport()
+	if err != nil {
+		return nil, err
+	}
+
+	return &PrometheusExecutor{
+		DataSource: dsInfo,
+		Transport:  transport,
+	}, nil
 }
 }
 
 
 var (
 var (
@@ -36,7 +48,8 @@ func init() {
 
 
 func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
 func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
 	cfg := prometheus.Config{
 	cfg := prometheus.Config{
-		Address: e.DataSourceInfo.Url,
+		Address:   e.DataSource.Url,
+		Transport: e.Transport,
 	}
 	}
 
 
 	client, err := prometheus.New(cfg)
 	client, err := prometheus.New(cfg)

+ 6 - 5
pkg/tsdb/testdata/testdata.go

@@ -4,19 +4,20 @@ import (
 	"context"
 	"context"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 )
 
 
 type TestDataExecutor struct {
 type TestDataExecutor struct {
-	*tsdb.DataSourceInfo
+	*models.DataSource
 	log log.Logger
 	log log.Logger
 }
 }
 
 
-func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
+func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) {
 	return &TestDataExecutor{
 	return &TestDataExecutor{
-		DataSourceInfo: dsInfo,
-		log:            log.New("tsdb.testdata"),
-	}
+		DataSource: dsInfo,
+		log:        log.New("tsdb.testdata"),
+	}, nil
 }
 }
 
 
 func init() {
 func init() {

+ 19 - 18
pkg/tsdb/tsdb_test.go

@@ -5,6 +5,7 @@ import (
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
+	"github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -15,9 +16,9 @@ func TestMetricQuery(t *testing.T) {
 		Convey("Given 3 queries for 2 data sources", func() {
 		Convey("Given 3 queries for 2 data sources", func() {
 			request := &Request{
 			request := &Request{
 				Queries: QuerySlice{
 				Queries: QuerySlice{
-					{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
-					{RefId: "B", DataSource: &DataSourceInfo{Id: 1}},
-					{RefId: "C", DataSource: &DataSourceInfo{Id: 2}},
+					{RefId: "A", DataSource: &models.DataSource{Id: 1}},
+					{RefId: "B", DataSource: &models.DataSource{Id: 1}},
+					{RefId: "C", DataSource: &models.DataSource{Id: 2}},
 				},
 				},
 			}
 			}
 
 
@@ -32,9 +33,9 @@ func TestMetricQuery(t *testing.T) {
 		Convey("Given query 2 depends on query 1", func() {
 		Convey("Given query 2 depends on query 1", func() {
 			request := &Request{
 			request := &Request{
 				Queries: QuerySlice{
 				Queries: QuerySlice{
-					{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
-					{RefId: "B", DataSource: &DataSourceInfo{Id: 2}},
-					{RefId: "C", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
+					{RefId: "A", DataSource: &models.DataSource{Id: 1}},
+					{RefId: "B", DataSource: &models.DataSource{Id: 2}},
+					{RefId: "C", DataSource: &models.DataSource{Id: 3}, Depends: []string{"A", "B"}},
 				},
 				},
 			}
 			}
 
 
@@ -56,7 +57,7 @@ func TestMetricQuery(t *testing.T) {
 	Convey("When executing request with one query", t, func() {
 	Convey("When executing request with one query", t, func() {
 		req := &Request{
 		req := &Request{
 			Queries: QuerySlice{
 			Queries: QuerySlice{
-				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+				{RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}},
 			},
 			},
 		}
 		}
 
 
@@ -75,8 +76,8 @@ func TestMetricQuery(t *testing.T) {
 	Convey("When executing one request with two queries from same data source", t, func() {
 	Convey("When executing one request with two queries from same data source", t, func() {
 		req := &Request{
 		req := &Request{
 			Queries: QuerySlice{
 			Queries: QuerySlice{
-				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
-				{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+				{RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}},
+				{RefId: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}},
 			},
 			},
 		}
 		}
 
 
@@ -101,9 +102,9 @@ func TestMetricQuery(t *testing.T) {
 	Convey("When executing one request with three queries from different datasources", t, func() {
 	Convey("When executing one request with three queries from different datasources", t, func() {
 		req := &Request{
 		req := &Request{
 			Queries: QuerySlice{
 			Queries: QuerySlice{
-				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
-				{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
-				{RefId: "C", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
+				{RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}},
+				{RefId: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}},
+				{RefId: "C", DataSource: &models.DataSource{Id: 2, Type: "test"}},
 			},
 			},
 		}
 		}
 
 
@@ -118,7 +119,7 @@ func TestMetricQuery(t *testing.T) {
 	Convey("When query uses data source of unknown type", t, func() {
 	Convey("When query uses data source of unknown type", t, func() {
 		req := &Request{
 		req := &Request{
 			Queries: QuerySlice{
 			Queries: QuerySlice{
-				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
+				{RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "asdasdas"}},
 			},
 			},
 		}
 		}
 
 
@@ -130,10 +131,10 @@ func TestMetricQuery(t *testing.T) {
 		req := &Request{
 		req := &Request{
 			Queries: QuerySlice{
 			Queries: QuerySlice{
 				{
 				{
-					RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
+					RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"},
 				},
 				},
 				{
 				{
-					RefId: "B", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
+					RefId: "B", DataSource: &models.DataSource{Id: 2, Type: "test"}, Depends: []string{"A"},
 				},
 				},
 			},
 			},
 		}
 		}
@@ -167,9 +168,9 @@ func TestMetricQuery(t *testing.T) {
 }
 }
 
 
 func registerFakeExecutor() *FakeExecutor {
 func registerFakeExecutor() *FakeExecutor {
-	executor := NewFakeExecutor(nil)
-	RegisterExecutor("test", func(dsInfo *DataSourceInfo) Executor {
-		return executor
+	executor, _ := NewFakeExecutor(nil)
+	RegisterExecutor("test", func(dsInfo *models.DataSource) (Executor, error) {
+		return executor, nil
 	})
 	})
 
 
 	return executor
 	return executor

+ 3 - 1
public/app/core/services/backend_srv.ts

@@ -74,7 +74,9 @@ export class BackendSrv {
     return this.$http(options).then(results => {
     return this.$http(options).then(results => {
       if (options.method !== 'GET') {
       if (options.method !== 'GET') {
         if (results && results.data.message) {
         if (results && results.data.message) {
-          this.alertSrv.set(results.data.message, '', 'success', 3000);
+          if (options.showSuccessAlert !== false) {
+            this.alertSrv.set(results.data.message, '', 'success', 3000);
+          }
         }
         }
       }
       }
       return results.data;
       return results.data;

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

@@ -10,6 +10,7 @@ export class User {
   isSignedIn: any;
   isSignedIn: any;
   orgRole: any;
   orgRole: any;
   timezone: string;
   timezone: string;
+  helpFlags1: number;
 
 
   constructor() {
   constructor() {
     if (config.bootData.user) {
     if (config.bootData.user) {

+ 1 - 0
public/app/core/services/keybindingSrv.ts

@@ -89,6 +89,7 @@ export class KeybindingSrv {
 
 
     this.bind('mod+o', () => {
     this.bind('mod+o', () => {
       dashboard.sharedCrosshair = !dashboard.sharedCrosshair;
       dashboard.sharedCrosshair = !dashboard.sharedCrosshair;
+      appEvents.emit('graph-hover-clear');
       scope.broadcastRefresh();
       scope.broadcastRefresh();
     });
     });
 
 

+ 1 - 0
public/app/features/alerting/alert_tab_ctrl.ts

@@ -94,6 +94,7 @@ export class AlertTabCtrl {
       case "victorops": return "fa fa-pagelines";
       case "victorops": return "fa fa-pagelines";
       case "webhook": return "fa fa-cubes";
       case "webhook": return "fa fa-cubes";
       case "pagerduty": return "fa fa-bullhorn";
       case "pagerduty": return "fa fa-bullhorn";
+      case "opsgenie": return "fa fa-bell";
     }
     }
   }
   }
 
 

+ 18 - 1
public/app/features/alerting/partials/notification_edit.html

@@ -19,7 +19,7 @@
       <div class="gf-form">
       <div class="gf-form">
         <span class="gf-form-label width-12">Type</span>
         <span class="gf-form-label width-12">Type</span>
         <div class="gf-form-select-wrapper width-15">
         <div class="gf-form-select-wrapper width-15">
-          <select class="gf-form-input" ng-model="ctrl.model.type" ng-options="t for t in ['webhook', 'email', 'slack', 'pagerduty', 'victorops']" ng-change="ctrl.typeChanged(notification, $index)">
+          <select class="gf-form-input" ng-model="ctrl.model.type" ng-options="t for t in ['webhook', 'email', 'slack', 'pagerduty', 'victorops', 'opsgenie']" ng-change="ctrl.typeChanged(notification, $index)">
           </select>
           </select>
         </div>
         </div>
       </div>
       </div>
@@ -122,6 +122,23 @@
       </div>
       </div>
     </div>
     </div>
 
 
+    <div class="gf-form-group" ng-if="ctrl.model.type === 'opsgenie'">
+      <h3 class="page-heading">OpsGenie settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-14">API Key</span>
+        <input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiKey" placeholder="OpsGenie API Key"></input>
+      </div>
+      <div class="gf-form">
+        <gf-form-switch
+           class="gf-form"
+           label="Auto close incidents"
+           label-class="width-14"
+           checked="ctrl.model.settings.autoClose"
+           tooltip="Automatically close alerts in OpseGenie once the alert goes back to ok.">
+        </gf-form-switch>
+      </div>
+    </div>
+
     <div class="gf-form-group">
     <div class="gf-form-group">
       <div class="gf-form-inline">
       <div class="gf-form-inline">
         <div class="gf-form width-6">
         <div class="gf-form width-6">

+ 4 - 4
public/app/features/dashboard/import/dash_import.html

@@ -123,11 +123,11 @@
       </div>
       </div>
 
 
       <div class="gf-form-button-row">
       <div class="gf-form-button-row">
-        <button type="button" class="btn gf-form-btn btn-success width-10" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
-          <i class="fa fa-save"></i> Save &amp; Open
+        <button type="button" class="btn gf-form-btn btn-success width-12" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
+          <i class="fa fa-save"></i> Import
         </button>
         </button>
-        <button type="button" class="btn gf-form-btn btn-danger width-10" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
-          <i class="fa fa-save"></i> Overwrite &amp; Open
+        <button type="button" class="btn gf-form-btn btn-danger width-12" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
+          <i class="fa fa-save"></i> Import (Overwrite)
         </button>
         </button>
         <a class="btn btn-link" ng-click="dismiss()">Cancel</a>
         <a class="btn btn-link" ng-click="dismiss()">Cancel</a>
         <a class="btn btn-link" ng-click="ctrl.back()">Back</a>
         <a class="btn btn-link" ng-click="ctrl.back()">Back</a>

+ 3 - 3
public/app/features/dashboard/partials/settings.html

@@ -57,13 +57,13 @@
         </gf-form-switch>
         </gf-form-switch>
 				<gf-form-switch class="gf-form"
 				<gf-form-switch class="gf-form"
                         label="Hide Controls"
                         label="Hide Controls"
-                        tooltip="Hide row controls. Shortcut: CTRL+H"
+                        tooltip="Hide row controls. Shortcut: CTRL+H or CMD+H"
                         checked="dashboard.hideControls"
                         checked="dashboard.hideControls"
                         label-class="width-11">
                         label-class="width-11">
         </gf-form-switch>
         </gf-form-switch>
         <gf-form-switch class="gf-form"
         <gf-form-switch class="gf-form"
-                        label="Shared Crosshair"
-                        tooltip="Shared Crosshair line on all graphs. Shortcut: CTRL+O"
+                        label="Shared Tooltip"
+                        tooltip="Shared Tooltip on all graphs. Shortcut: CTRL+O or CMD+O"
                         checked="dashboard.sharedCrosshair"
                         checked="dashboard.sharedCrosshair"
                         label-class="width-11">
                         label-class="width-11">
         </gf-form-switch>
         </gf-form-switch>

+ 8 - 5
public/app/features/dashboard/row/add_panel.ts

@@ -18,9 +18,15 @@ export class AddPanelCtrl {
   constructor(private $scope, private $timeout, private $rootScope) {
   constructor(private $scope, private $timeout, private $rootScope) {
     this.row = this.rowCtrl.row;
     this.row = this.rowCtrl.row;
     this.dashboard = this.rowCtrl.dashboard;
     this.dashboard = this.rowCtrl.dashboard;
-    this.allPanels = _.orderBy(_.map(config.panels, item => item), 'sort');
-    this.panelHits = this.allPanels;
     this.activeIndex = 0;
     this.activeIndex = 0;
+
+    this.allPanels = _.chain(config.panels)
+      .filter({hideFromList: false})
+      .map(item => item)
+      .orderBy('sort')
+      .value();
+
+    this.panelHits = this.allPanels;
   }
   }
 
 
   keyDown(evt) {
   keyDown(evt) {
@@ -78,11 +84,8 @@ export class AddPanelCtrl {
     var panel = {
     var panel = {
       id: null,
       id: null,
       title: config.new_panel_title,
       title: config.new_panel_title,
-      error: false,
       span: span < defaultSpan && span > 0 ? span : defaultSpan,
       span: span < defaultSpan && span > 0 ? span : defaultSpan,
-      editable: true,
       type: panelPluginInfo.id,
       type: panelPluginInfo.id,
-      isNew: true,
     };
     };
 
 
     this.rowCtrl.closeDropView();
     this.rowCtrl.closeDropView();

+ 14 - 2
public/app/features/plugins/ds_edit_ctrl.ts

@@ -28,6 +28,7 @@ export class DataSourceEditCtrl {
   tabIndex: number;
   tabIndex: number;
   hasDashboards: boolean;
   hasDashboards: boolean;
   editForm: any;
   editForm: any;
+  gettingStarted: boolean;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(
   constructor(
@@ -46,12 +47,23 @@ export class DataSourceEditCtrl {
         if (this.$routeParams.id) {
         if (this.$routeParams.id) {
           this.getDatasourceById(this.$routeParams.id);
           this.getDatasourceById(this.$routeParams.id);
         } else {
         } else {
-          this.current = angular.copy(defaults);
-          this.typeChanged();
+          this.initNewDatasourceModel();
         }
         }
       });
       });
     }
     }
 
 
+    initNewDatasourceModel() {
+      this.current = angular.copy(defaults);
+
+      // We are coming from getting started
+      if (this.$location.search().gettingstarted) {
+        this.gettingStarted = true;
+        this.current.isDefault = true;
+      }
+
+      this.typeChanged();
+    }
+
     loadDatasourceTypes() {
     loadDatasourceTypes() {
       if (datasourceTypes.length > 0) {
       if (datasourceTypes.length > 0) {
         this.types = datasourceTypes;
         this.types = datasourceTypes;

+ 2 - 2
public/app/partials/dashboard.html

@@ -15,8 +15,8 @@
 	</div>
 	</div>
 
 
 	<div ng-show='dashboardMeta.canEdit' class="row-fluid add-row-panel-hint">
 	<div ng-show='dashboardMeta.canEdit' class="row-fluid add-row-panel-hint">
-		<div class="span12" style="text-align:right;">
-			<span style="margin-right: 10px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
+		<div class="span12" style="text-align:left;">
+			<span style="margin-left: 12px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
 				<span><i class="fa fa-plus"></i> ADD ROW</span>
 				<span><i class="fa fa-plus"></i> ADD ROW</span>
 			</span>
 			</span>
 		</div>
 		</div>

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

@@ -57,16 +57,23 @@
 			<label class="gf-form-label width-10">Order</label>
 			<label class="gf-form-label width-10">Order</label>
 			<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 			<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 		</div>
 		</div>
-
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Size</label>
 			<label class="gf-form-label width-10">Size</label>
 			<metric-segment-model property="agg.settings.size" options="sizeOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 			<metric-segment-model property="agg.settings.size" options="sizeOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 		</div>
 		</div>
-
 		<div class="gf-form offset-width-7">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Order By</label>
 			<label class="gf-form-label width-10">Order By</label>
 			<metric-segment-model property="agg.settings.orderBy" options="orderByOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 			<metric-segment-model property="agg.settings.orderBy" options="orderByOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 		</div>
 		</div>
+		<div class="gf-form offset-width-7">
+			<label class="gf-form-label width-10">
+				Missing
+				<info-popover mode="right-normal">
+					The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value
+				</info-popover>
+			</label>
+			<input type="text" class="gf-form-input max-width-12" empty-to-null ng-model="agg.settings.missing" ng-blur="onChangeInternal()" spellcheck='false'>
+		</div>
 	</div>
 	</div>
 
 
 	<div ng-if="agg.type === 'filters'">
 	<div ng-if="agg.type === 'filters'">
@@ -94,5 +101,3 @@
 	</div>
 	</div>
 
 
 </div>
 </div>
-
-

+ 5 - 0
public/app/plugins/datasource/elasticsearch/partials/metric_agg.html

@@ -53,6 +53,11 @@
 		<input type="text" class="gf-form-input max-width-12" ng-change="onChangeInternal()" ng-model="agg.settings.model" blur="onChange()" spellcheck='false'>
 		<input type="text" class="gf-form-input max-width-12" ng-change="onChangeInternal()" ng-model="agg.settings.model" blur="onChange()" spellcheck='false'>
 	</div>
 	</div>
 
 
+	<div class="gf-form offset-width-7" ng-if="agg.type === 'moving_avg'">
+		<label class="gf-form-label width-10">Predict</label>
+		<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.predict" ng-blur="onChangeInternal()" spellcheck='false'>
+	</div>
+
 	<div class="gf-form offset-width-7" ng-if="agg.type === 'percentiles'">
 	<div class="gf-form offset-width-7" ng-if="agg.type === 'percentiles'">
 		<label class="gf-form-label width-10">Percentiles</label>
 		<label class="gf-form-label width-10">Percentiles</label>
 		<input type="text" class="gf-form-input max-width-12" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>
 		<input type="text" class="gf-form-input max-width-12" ng-model="agg.settings.percents" array-join ng-blur="onChange()"></input>

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

@@ -49,6 +49,10 @@ function (queryDef) {
       }
       }
     }
     }
 
 
+    if (aggDef.settings.missing) {
+      queryNode.terms.missing = aggDef.settings.missing;
+    }
+
     return queryNode;
     return queryNode;
   };
   };
 
 
@@ -65,6 +69,10 @@ function (queryDef) {
       esAgg.interval = "$interval";
       esAgg.interval = "$interval";
     }
     }
 
 
+    if (settings.missing) {
+      esAgg.missing = settings.missing;
+    }
+
     return esAgg;
     return esAgg;
   };
   };
 
 

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

@@ -72,7 +72,8 @@ function (_) {
     pipelineOptions: {
     pipelineOptions: {
       'moving_avg' : [
       'moving_avg' : [
         {text: 'window', default: 5},
         {text: 'window', default: 5},
-        {text: 'model', default: 'simple'}
+        {text: 'model', default: 'simple'},
+        {text: 'predict', default: 0}
       ],
       ],
       'derivative': [
       'derivative': [
         {text: 'unit', default: undefined},
         {text: 'unit', default: undefined},

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

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

+ 40 - 0
public/app/plugins/panel/gettingstarted/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/gettingstarted/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>

+ 19 - 0
public/app/plugins/panel/gettingstarted/module.html

@@ -0,0 +1,19 @@
+<div class="dashlist" ng-if="ctrl.checksDone">
+  <div class="dashlist-section">
+    <h6 class="dashlist-section-header">
+      Getting Started with Grafana
+      <button class="dashlist-cta-close-btn" ng-click="ctrl.dismiss()">
+        <i class="fa fa-remove"></i>
+      </button>
+    </h6>
+    <ul class="progress-tracker">
+      <li class="progress-step" ng-repeat="step in ctrl.steps" ng-class="step.cssClass">
+        <a class="progress-link" ng-href="{{step.href}}" target="{{step.target}}" title="{{step.note}}">
+          <span class="progress-marker" ng-class="step.cssClass"><i class="{{step.icon}}"></i></span>
+          <span class="progress-text" ng-href="{{step.href}}" target="{{step.target}}">{{step.title}}</span>
+        </a>
+        <a class="btn progress-step-cta" ng-href="{{step.href}}" target="{{step.target}}">{{step.cta}}</a>
+      </li>
+    </ul>
+  </div>
+</div>

+ 119 - 0
public/app/plugins/panel/gettingstarted/module.ts

@@ -0,0 +1,119 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import {PanelCtrl} from 'app/plugins/sdk';
+
+import {contextSrv} from 'app/core/core';
+
+class GettingStartedPanelCtrl extends PanelCtrl {
+  static templateUrl = 'public/app/plugins/panel/gettingstarted/module.html';
+  checksDone: boolean;
+  stepIndex: number;
+  steps: any;
+
+  /** @ngInject **/
+  constructor($scope, $injector, private backendSrv, private datasourceSrv, private $q) {
+    super($scope, $injector);
+
+    this.stepIndex = 0;
+    this.steps = [];
+
+    this.steps.push({
+      title: 'Install Grafana',
+      icon: 'icon-gf icon-gf-check',
+      href: 'http://docs.grafana.org/',
+      target: '_blank',
+      note: 'Review the installation docs',
+      check: () => $q.when(true),
+    });
+
+    this.steps.push({
+      title: 'Create your first data source',
+      cta: 'Add data source',
+      icon: 'icon-gf icon-gf-datasources',
+      href: 'datasources/new?gettingstarted',
+      check: () => {
+        return $q.when(
+          datasourceSrv.getMetricSources().filter(item => {
+            return item.meta.builtIn === false;
+          }).length > 0
+        );
+      }
+    });
+
+    this.steps.push({
+      title: 'Create your first dashboard',
+      cta: 'New dashboard',
+      icon: 'icon-gf icon-gf-dashboard',
+      href: 'dashboard/new?gettingstarted',
+      check: () => {
+        return this.backendSrv.search({limit: 1}).then(result => {
+          return result.length > 0;
+        });
+      }
+    });
+
+    this.steps.push({
+      title: 'Invite your team',
+      cta: 'Add Users',
+      icon: 'icon-gf icon-gf-users',
+      href: 'org/users?gettingstarted',
+      check: () => {
+        return  this.backendSrv.get('api/org/users').then(res => {
+          return res.length > 1;
+        });
+      }
+    });
+
+
+    this.steps.push({
+      title: 'Install apps & plugins',
+      cta: 'Explore plugin repository',
+      icon: 'icon-gf icon-gf-apps',
+      href: 'https://grafana.net/plugins?utm_source=grafana_getting_started',
+      check: () => {
+        return this.backendSrv.get('api/plugins', {embedded: 0, core: 0}).then(plugins => {
+          return plugins.length > 0;
+        });
+      }
+    });
+  }
+
+  $onInit() {
+    this.stepIndex = -1;
+    return this.nextStep().then(res => {
+      this.checksDone = true;
+    });
+  }
+
+  nextStep() {
+    if (this.stepIndex === this.steps.length - 1) {
+      return this.$q.when();
+    }
+
+    this.stepIndex += 1;
+    var currentStep = this.steps[this.stepIndex];
+    return currentStep.check().then(passed => {
+      if (passed) {
+        currentStep.cssClass = 'completed';
+        return this.nextStep();
+      }
+
+      currentStep.cssClass = 'active';
+      return this.$q.when();
+    });
+  }
+
+  dismiss() {
+    this.row.removePanel(this.panel, false);
+
+    this.backendSrv.request({
+      method: 'PUT',
+      url: '/api/user/helpflags/1',
+      showSuccessAlert: false,
+    }).then(res => {
+      contextSrv.user.helpFlags1 = res.helpFlags1;
+    });
+  }
+}
+
+export {GettingStartedPanelCtrl, GettingStartedPanelCtrl as PanelCtrl}

+ 18 - 0
public/app/plugins/panel/gettingstarted/plugin.json

@@ -0,0 +1,18 @@
+{
+  "type": "panel",
+  "name": "Getting Started",
+  "id": "gettingstarted",
+
+  "hideFromList": true,
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "http://grafana.org"
+    },
+    "logos": {
+      "small": "img/icn-dashlist-panel.svg",
+      "large": "img/icn-dashlist-panel.svg"
+    }
+  }
+}

+ 31 - 30
public/app/plugins/panel/graph/graph.ts

@@ -9,18 +9,17 @@ import 'jquery.flot.fillbelow';
 import 'jquery.flot.crosshair';
 import 'jquery.flot.crosshair';
 import './jquery.flot.events';
 import './jquery.flot.events';
 
 
-import angular from 'angular';
 import $ from 'jquery';
 import $ from 'jquery';
-import moment from 'moment';
 import _ from 'lodash';
 import _ from 'lodash';
+import moment from 'moment';
 import kbn from   'app/core/utils/kbn';
 import kbn from   'app/core/utils/kbn';
+import {appEvents, coreModule} from 'app/core/core';
 import GraphTooltip from './graph_tooltip';
 import GraphTooltip from './graph_tooltip';
 import {ThresholdManager} from './threshold_manager';
 import {ThresholdManager} from './threshold_manager';
 
 
-var module = angular.module('grafana.directives');
 var labelWidthCache = {};
 var labelWidthCache = {};
 
 
-module.directive('grafanaGraph', function($rootScope, timeSrv) {
+coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
   return {
   return {
     restrict: 'A',
     restrict: 'A',
     template: '',
     template: '',
@@ -28,14 +27,19 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
       var ctrl = scope.ctrl;
       var ctrl = scope.ctrl;
       var dashboard = ctrl.dashboard;
       var dashboard = ctrl.dashboard;
       var panel = ctrl.panel;
       var panel = ctrl.panel;
-      var data, annotations;
+      var data;
+      var annotations;
+      var plot;
       var sortedSeries;
       var sortedSeries;
       var legendSideLastValue = null;
       var legendSideLastValue = null;
       var rootScope = scope.$root;
       var rootScope = scope.$root;
       var panelWidth = 0;
       var panelWidth = 0;
       var thresholdManager = new ThresholdManager(ctrl);
       var thresholdManager = new ThresholdManager(ctrl);
-      var plot;
+      var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
+        return sortedSeries;
+      });
 
 
+      // panel events
       ctrl.events.on('panel-teardown', () => {
       ctrl.events.on('panel-teardown', () => {
         thresholdManager = null;
         thresholdManager = null;
 
 
@@ -45,34 +49,35 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
         }
         }
       });
       });
 
 
-      rootScope.onAppEvent('setCrosshair', function(event, info) {
-        // do not need to to this if event is from this panel
-        if (info.scope === scope) {
+      ctrl.events.on('render', function(renderData) {
+        data = renderData || data;
+        if (!data) {
           return;
           return;
         }
         }
+        annotations = ctrl.annotations;
+        render_panel();
+      });
 
 
-        if (dashboard.sharedCrosshair) {
-          if (plot) {
-            plot.setCrosshair({ x: info.pos.x, y: info.pos.y });
-          }
+      // global events
+      appEvents.on('graph-hover', function(evt) {
+        // ignore other graph hover events if shared tooltip is disabled
+        if (!dashboard.sharedCrosshair) {
+          return;
         }
         }
-      }, scope);
 
 
-      rootScope.onAppEvent('clearCrosshair', function() {
-        if (plot) {
-          plot.clearCrosshair();
+        // ignore if we are the emitter
+        if (!plot || evt.panel.id === panel.id || ctrl.otherPanelInFullscreenMode()) {
+          return;
         }
         }
+
+        tooltip.show(evt.pos);
       }, scope);
       }, scope);
 
 
-      // Receive render events
-      ctrl.events.on('render', function(renderData) {
-        data = renderData || data;
-        if (!data) {
-          return;
+      appEvents.on('graph-hover-clear', function(event, info) {
+        if (plot) {
+          tooltip.clear(plot);
         }
         }
-        annotations = ctrl.annotations;
-        render_panel();
-      });
+      }, scope);
 
 
       function getLegendHeight(panelHeight) {
       function getLegendHeight(panelHeight) {
         if (!panel.legend.show || panel.legend.rightSide) {
         if (!panel.legend.show || panel.legend.rightSide) {
@@ -272,7 +277,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
             color: '#666'
             color: '#666'
           },
           },
           crosshair: {
           crosshair: {
-            mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null
+            mode: 'x'
           }
           }
         };
         };
 
 
@@ -565,10 +570,6 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
         return "%H:%M";
         return "%H:%M";
       }
       }
 
 
-      var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
-        return sortedSeries;
-      });
-
       elem.bind("plotselected", function (event, ranges) {
       elem.bind("plotselected", function (event, ranges) {
         scope.$apply(function() {
         scope.$apply(function() {
           timeSrv.setTime({
           timeSrv.setTime({

+ 37 - 14
public/app/plugins/panel/graph/graph_tooltip.js

@@ -1,16 +1,18 @@
 define([
 define([
   'jquery',
   'jquery',
-  'lodash'
+  'app/core/core',
 ],
 ],
-function ($) {
+function ($, core) {
   'use strict';
   'use strict';
 
 
+  var appEvents = core.appEvents;
+
   function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
   function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
     var self = this;
     var self = this;
     var ctrl = scope.ctrl;
     var ctrl = scope.ctrl;
     var panel = ctrl.panel;
     var panel = ctrl.panel;
 
 
-    var $tooltip = $('<div id="tooltip" class="graph-tooltip">');
+    var $tooltip = $('<div class="graph-tooltip">');
 
 
     this.destroy = function() {
     this.destroy = function() {
       $tooltip.remove();
       $tooltip.remove();
@@ -41,7 +43,7 @@ function ($) {
       return j - 1;
       return j - 1;
     };
     };
 
 
-    this.showTooltip = function(absoluteTime, innerHtml, pos, xMode) {
+    this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
       if (xMode === 'time') {
       if (xMode === 'time') {
         innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml;
         innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml;
       }
       }
@@ -140,22 +142,43 @@ function ($) {
           plot.unhighlight();
           plot.unhighlight();
         }
         }
       }
       }
-
-      if (dashboard.sharedCrosshair) {
-        ctrl.publishAppEvent('clearCrosshair');
-      }
+      appEvents.emit('graph-hover-clear');
     });
     });
 
 
     elem.bind("plothover", function (event, pos, item) {
     elem.bind("plothover", function (event, pos, item) {
+      self.show(pos, item);
+
+      // broadcast to other graph panels that we are hovering!
+      pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
+      appEvents.emit('graph-hover', {pos: pos, panel: panel});
+    });
+
+    this.clear = function(plot) {
+      $tooltip.detach();
+      plot.clearCrosshair();
+    };
+
+    this.show = function(pos, item) {
       var plot = elem.data().plot;
       var plot = elem.data().plot;
       var plotData = plot.getData();
       var plotData = plot.getData();
       var xAxes = plot.getXAxes();
       var xAxes = plot.getXAxes();
       var xMode = xAxes[0].options.mode;
       var xMode = xAxes[0].options.mode;
       var seriesList = getSeriesFn();
       var seriesList = getSeriesFn();
+      var allSeriesMode = panel.tooltip.shared;
       var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
       var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
 
 
-      if (dashboard.sharedCrosshair) {
-        ctrl.publishAppEvent('setCrosshair', {pos: pos, scope: scope});
+      // if panelRelY is defined another panel wants us to show a tooltip
+      // get pageX from position on x axis and pageY from relative position in original panel
+      if (pos.panelRelY) {
+        var pointOffset = plot.pointOffset({x: pos.x});
+        if (Number.isNaN(pointOffset.left) || pointOffset.left < 0) {
+          $tooltip.detach();
+          return;
+        }
+        pos.pageX = elem.offset().left + pointOffset.left;
+        pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
+        plot.setCrosshair(pos);
+        allSeriesMode = true;
       }
       }
 
 
       if (seriesList.length === 0) {
       if (seriesList.length === 0) {
@@ -168,7 +191,7 @@ function ($) {
         tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
         tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
       }
       }
 
 
-      if (panel.tooltip.shared) {
+      if (allSeriesMode) {
         plot.unhighlight();
         plot.unhighlight();
 
 
         var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
         var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
@@ -211,7 +234,7 @@ function ($) {
           plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
           plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
         }
         }
 
 
-        self.showTooltip(absoluteTime, seriesHtml, pos, xMode);
+        self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
       }
       }
       // single series tooltip
       // single series tooltip
       else if (item) {
       else if (item) {
@@ -232,13 +255,13 @@ function ($) {
 
 
         group += '<div class="graph-tooltip-value">' + value + '</div>';
         group += '<div class="graph-tooltip-value">' + value + '</div>';
 
 
-        self.showTooltip(absoluteTime, group, pos, xMode);
+        self.renderAndShow(absoluteTime, group, pos, xMode);
       }
       }
       // no hit
       // no hit
       else {
       else {
         $tooltip.detach();
         $tooltip.detach();
       }
       }
-    });
+    };
   }
   }
 
 
   return GraphTooltip;
   return GraphTooltip;

+ 1 - 2
public/app/plugins/panel/graph/module.ts

@@ -88,7 +88,7 @@ class GraphCtrl extends MetricsPanelCtrl {
       avg: false
       avg: false
     },
     },
     // how null points should be handled
     // how null points should be handled
-    nullPointMode : 'connected',
+    nullPointMode : 'null',
     // staircase line mode
     // staircase line mode
     steppedLine: false,
     steppedLine: false,
     // tooltip options
     // tooltip options
@@ -96,7 +96,6 @@ class GraphCtrl extends MetricsPanelCtrl {
       value_type: 'individual',
       value_type: 'individual',
       shared: true,
       shared: true,
       sort: 0,
       sort: 0,
-      msResolution: false,
     },
     },
     // time overrides
     // time overrides
     timeFrom: null,
     timeFrom: null,

+ 1 - 1
public/app/plugins/panel/graph/specs/graph_specs.ts

@@ -12,7 +12,7 @@ import {Emitter} from 'app/core/core';
 
 
 describe('grafanaGraph', function() {
 describe('grafanaGraph', function() {
 
 
-  beforeEach(angularMocks.module('grafana.directives'));
+  beforeEach(angularMocks.module('grafana.core'));
 
 
   function graphScenario(desc, func, elementWidth = 500)  {
   function graphScenario(desc, func, elementWidth = 500)  {
     describe(desc, function() {
     describe(desc, function() {

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

@@ -48,7 +48,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 		<div class="section gf-form-group">
 		<div class="section gf-form-group">
-			<h5 class="section-heading">Hover info</h5>
+			<h5 class="section-heading">Hover tooltip</h5>
 			<div class="gf-form">
 			<div class="gf-form">
 				<label class="gf-form-label width-9">Mode</label>
 				<label class="gf-form-label width-9">Mode</label>
 				<div class="gf-form-select-wrapper max-width-8">
 				<div class="gf-form-select-wrapper max-width-8">

+ 1 - 2
public/app/plugins/panel/pluginlist/module.ts

@@ -11,8 +11,7 @@ class PluginListCtrl extends PanelCtrl {
   viewModel: any;
   viewModel: any;
 
 
   // Set and populate defaults
   // Set and populate defaults
-  panelDefaults = {
-  };
+  panelDefaults = {};
 
 
   /** @ngInject */
   /** @ngInject */
   constructor($scope, $injector, private backendSrv, private $location) {
   constructor($scope, $injector, private backendSrv, private $location) {

+ 2 - 2
public/dashboards/home.json

@@ -9,6 +9,7 @@
   "sharedCrosshair": false,
   "sharedCrosshair": false,
   "rows": [
   "rows": [
    {
    {
+      "title": "Home Dashboard",
       "collapse": false,
       "collapse": false,
       "editable": true,
       "editable": true,
       "height": "25px",
       "height": "25px",
@@ -25,8 +26,7 @@
           "transparent": true,
           "transparent": true,
           "type": "text"
           "type": "text"
         }
         }
-      ],
-      "title": "New row"
+     ]
    },
    },
    {
    {
       "collapse": false,
       "collapse": false,

+ 1 - 0
public/sass/_grafana.scss

@@ -43,6 +43,7 @@
 @import "components/submenu";
 @import "components/submenu";
 @import "components/panel_alertlist";
 @import "components/panel_alertlist";
 @import "components/panel_dashlist";
 @import "components/panel_dashlist";
+@import "components/panel_gettingstarted";
 @import "components/panel_pluginlist";
 @import "components/panel_pluginlist";
 @import "components/panel_singlestat";
 @import "components/panel_singlestat";
 @import "components/panel_table";
 @import "components/panel_table";

+ 171 - 0
public/sass/components/_panel_gettingstarted.scss

@@ -0,0 +1,171 @@
+
+// Colours
+$progress-color-dark:       $panel-bg !default;
+$progress-color:            $panel-bg !default;
+$progress-color-light:      $panel-bg !default;
+$progress-color-grey-light: $body-bg !default;
+$progress-color-shadow:     $panel-border !default;
+$progress-color-grey:       $iconContainerBackground !default;
+$progress-color-grey-dark:  $iconContainerBackground !default;
+
+// Sizing
+$marker-size:               60px !default;
+$marker-size-half:          ($marker-size / 2);
+$path-height:               2px !default;
+$path-position:             $marker-size-half - ($path-height / 2);
+
+
+.dashlist-cta-close-btn {
+  color: $text-color-weak;
+  float: right;
+  padding: 0;
+  margin: 0 2px 0 0;
+  background-color: transparent;
+  border: none;
+
+  i {
+    font-size: 80%;
+  }
+
+  &:hover {
+    color: $white;
+  }
+}
+
+// Container element
+.progress-tracker {
+  display: flex;
+  margin: 20px auto;
+  padding: 0;
+  list-style: none;
+}
+
+// Step container that creates lines between steps
+.progress-step {
+  text-align: center;
+  position: relative;
+  flex: 1 1 0%;
+  margin: 0;
+  padding: 0;
+  color: $text-color-weak;
+
+  // For a flexbox bug in firefox that wont allow the text overflow on the text
+  min-width: $marker-size;
+
+   &::after {
+    right: -50%;
+    content: '';
+    display: block;
+    position: absolute;
+    z-index: 1;
+    top: $path-position;
+    bottom: $path-position;
+    right: - $marker-size-half;
+    width: 100%;
+    height: $path-height;
+    border-top: 2px solid $progress-color-grey-light;
+    border-bottom: $progress-color-shadow;
+    background: $progress-color-grey-light;
+  }
+
+  &:first-child {
+    &::after {
+      left: 50%;
+    }
+  }
+  &:last-child {
+    &::after {
+      right: 50%;
+    }
+  }
+
+  // Active state
+  &.active {
+    .progress-step-cta {
+      display: inline-block;
+    }
+    .progress-title {
+      font-weight: 400;
+    }
+    .progress-text {
+      display: none;
+    }
+    .progress-marker {
+      .icon-gf {
+        color: $brand-primary;
+        -webkit-text-fill-color: transparent;
+        background: $brand-gradient;
+        -webkit-background-clip: text;
+        text-decoration:none;
+      }
+    }
+  }
+
+  &.completed {
+    .progress-marker {
+      color: $online;
+
+      // change icon to check
+      .icon-gf::before {
+        content: "\e604";
+      }
+    }
+    .progress-text {
+      text-decoration: line-through;
+    }
+    &::after {
+      background: $progress-color-grey-light;
+    }
+  }
+}
+
+.progress-step-cta {
+  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $btn-border-radius);
+  @include buttonBackground($btn-success-bg, $btn-success-bg-hl);
+  display: none;
+}
+
+// Progress marker
+.progress-marker {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+  width: $marker-size;
+  height: $marker-size;
+  padding-bottom: 2px; // To align text within the marker
+  z-index: 20;
+  background-color: $panel-bg;
+  margin-left: auto;
+  margin-right: auto;
+  margin-bottom: $spacer;
+  color: $text-color-weak;
+  font-size: 35px;
+  vertical-align: sub;
+}
+
+// Progress text
+.progress-text {
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  color: $text-muted;
+}
+
+.progress-marker {
+  color: $text-color-weak;
+  text-decoration:none;
+  font-size: 35px;
+  vertical-align: sub;
+}
+
+a.progress-link {
+  &:hover {
+    .progress-marker, .progress-text {
+      color: $link-hover-color;
+    }
+  &:hover .progress-marker.completed {
+      color: $online;
+    }
+  }
+}

+ 1 - 1
public/sass/components/_tabs.scss

@@ -65,6 +65,6 @@
     border-bottom: 2px solid $panel-bg;
     border-bottom: 2px solid $panel-bg;
     color: $link-color;
     color: $link-color;
     position: relative;
     position: relative;
-    top: 2px;
+    top: 1px;
   }
   }
 }
 }