Browse Source

Merge branch 'master' into elastic5_support

bergquist 9 years ago
parent
commit
619c5c4f1b
76 changed files with 1376 additions and 506 deletions
  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)
 
-### 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
 * **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)
 * **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)
+* **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
 * **Playlist**: Add support for kiosk mode [#6727](https://github.com/grafana/grafana/issues/6727)
 
 ### Bugfixes
 * **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 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)

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

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

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

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

@@ -14,14 +14,14 @@ weight = 1
 
 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
 
 ```
-$ 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 dpkg -i grafana_4.0.1-1480694114_amd64.deb
+$ sudo dpkg -i grafana_4.0.2-1481203731_amd64.deb
 ```
 
 ## APT Repository

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

@@ -14,24 +14,24 @@ weight = 2
 
 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
 
 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`.
 
 #### On CentOS / Fedora / Redhat:
 
     $ 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:
 
-    $ 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
 

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

@@ -13,7 +13,7 @@ weight = 3
 
 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
 

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

@@ -9,7 +9,7 @@ weight = 8
 
 # 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
 
@@ -22,9 +22,9 @@ The export feature is accessed from the share menu.
 ### Making a dashboard portable
 
 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.
 
 ## 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
 
-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">
 

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

@@ -1,5 +1,5 @@
 +++
-title = "Singletat Panel"
+title = "Singlestat Panel"
 keywords = ["grafana", "dashboard", "documentation", "panels", "singlestat"]
 type = "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
-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
 

+ 4 - 1
pkg/api/api.go

@@ -113,6 +113,9 @@ func Register(r *macaron.Macaron) {
 
 			r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
 			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.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
@@ -193,7 +196,7 @@ func Register(r *macaron.Macaron) {
 		r.Group("/datasources", func() {
 			r.Get("/", GetDataSources)
 			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.Get("/:id", wrap(GetDataSourceById))
 			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/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"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)
 	}
 
+	if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
+		addGettingStartedPanelToHomeDashboard(dash.Dashboard)
+	}
+
 	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) {
 	file := c.Params(":file")
 

+ 1 - 74
pkg/api/dataproxy.go

@@ -1,13 +1,9 @@
 package api
 
 import (
-	"crypto/tls"
-	"crypto/x509"
-	"net"
 	"net/http"
 	"net/http/httputil"
 	"net/url"
-	"sync"
 	"time"
 
 	"github.com/grafana/grafana/pkg/api/cloudwatch"
@@ -19,75 +15,6 @@ import (
 	"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 {
 	director := func(req *http.Request) {
 		req.URL.Scheme = targetUrl.Scheme
@@ -189,7 +116,7 @@ func ProxyDataSourceRequest(c *middleware.Context) {
 	}
 
 	proxy := NewReverseProxy(ds, proxyPath, targetUrl)
-	proxy.Transport, err = DataProxyTransport(ds)
+	proxy.Transport, err = ds.GetHttpTransport()
 	if err != nil {
 		c.JsonApiErr(400, "Unable to load TLS certificate", err)
 		return

+ 1 - 152
pkg/api/dataproxy_test.go

@@ -4,24 +4,18 @@ import (
 	"net/http"
 	"net/url"
 	"testing"
-	"time"
 
 	. "github.com/smartystreets/goconvey/convey"
 
-	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/setting"
-	"github.com/grafana/grafana/pkg/util"
 )
 
 func TestDataSourceProxy(t *testing.T) {
-
 	Convey("When getting graphite datasource proxy", t, func() {
-		clearCache()
 		ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
 		targetUrl, err := url.Parse(ds.Url)
 		proxy := NewReverseProxy(&ds, "/render", targetUrl)
-		proxy.Transport, err = DataProxyTransport(&ds)
+		proxy.Transport, err = ds.GetHttpTransport()
 		So(err, ShouldBeNil)
 
 		transport, ok := proxy.Transport.(*http.Transport)
@@ -40,7 +34,6 @@ func TestDataSourceProxy(t *testing.T) {
 	})
 
 	Convey("When getting influxdb datasource proxy", t, func() {
-		clearCache()
 		ds := m.DataSource{
 			Type:     m.DS_INFLUXDB_08,
 			Url:      "http://influxdb:8083",
@@ -67,148 +60,4 @@ func TestDataSourceProxy(t *testing.T) {
 			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/bus"
-	"github.com/grafana/grafana/pkg/plugins"
-	//"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
 	"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 Json(200, "Datasource updated")
+	return Json(200, util.DynMap{"message": "Datasource updated"})
 }
 
 func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {

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

@@ -22,19 +22,20 @@ type LoginCommand 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 {

+ 7 - 6
pkg/api/frontendsettings.go

@@ -122,12 +122,13 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 	panels := map[string]interface{}{}
 	for _, panel := range enabledPlugins.Panels {
 		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",
 			Timezone:       prefs.Timezone,
 			Locale:         locale,
+			HelpFlags1:     c.HelpFlags1,
 		},
 		Settings:                settings,
 		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/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb/testdata"
 	"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),
 			IntervalMs:    query.Get("intervalMs").MustInt64(1000),
 			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)
 }
+
+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_PagerDuty Counter
 	M_Alerting_Notification_Sent_Victorops Counter
+	M_Alerting_Notification_Sent_OpsGenie  Counter
 
 	// Timers
 	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_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
 	M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
+	M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
 
 	// Timers
 	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)
 }
 
+func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool {
+	return ctx.HelpFlags1.HasFlag(flag)
+}
+
 func (ctx *Context) TimeRequest(timer metrics.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
 	EmailVerified bool
 	Theme         string
+	HelpFlags1    HelpFlags1
 
 	IsAdmin bool
 	OrgId   int64
@@ -144,6 +145,7 @@ type SignedInUser struct {
 	Email          string
 	ApiKeyId       int64
 	IsGrafanaAdmin bool
+	HelpFlags1     HelpFlags1
 }
 
 type UserProfileDTO struct {

+ 1 - 0
pkg/plugins/models.go

@@ -38,6 +38,7 @@ type PluginBase struct {
 	Includes     []*PluginInclude   `json:"includes"`
 	Module       string             `json:"module"`
 	BaseUrl      string             `json:"baseUrl"`
+	HideFromList bool               `json:"hideFromList"`
 
 	IncludedInAppId 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,
 		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) {
 	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() {
 			result := testReducer("sum", 1, 2, 3)
 			So(result, ShouldEqual, float64(6))
@@ -69,6 +50,25 @@ func TestSimpleReducer(t *testing.T) {
 			result := testReducer("median", 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("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", SetUsingOrg)
 	bus.AddHandler("sql", UpdateUserPermissions)
+	bus.AddHandler("sql", SetUserHelpFlag)
 }
 
 func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *session) (int64, error) {
@@ -207,7 +208,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
 	if err != nil {
 		return err
 	} else if has == false {
-		return  m.ErrUserNotFound
+		return m.ErrUserNotFound
 	}
 
 	query.Result = user
@@ -308,6 +309,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
 	                u.email        as email,
 	                u.login        as login,
 									u.name         as name,
+									u.help_flags1  as help_flags1,
 	                org.name       as org_name,
 	                org_user.role  as org_role,
 	                org.id         as org_id
@@ -380,3 +382,20 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error {
 		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
 
-import (
-	"context"
-	"errors"
-)
+import "context"
 
 type Batch struct {
 	DataSourceId int64
@@ -24,12 +21,12 @@ func newBatch(dsId int64, queries QuerySlice) *Batch {
 }
 
 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
 		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),
 		}
 		for _, query := range bg.Queries {

+ 16 - 6
pkg/tsdb/executor.go

@@ -1,6 +1,11 @@
 package tsdb
 
-import "context"
+import (
+	"context"
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/models"
+)
 
 type Executor interface {
 	Execute(ctx context.Context, queries QuerySlice, query *QueryContext) *BatchResult
@@ -8,17 +13,22 @@ type Executor interface {
 
 var registry map[string]GetExecutorFn
 
-type GetExecutorFn func(dsInfo *DataSourceInfo) Executor
+type GetExecutorFn func(dsInfo *models.DataSource) (Executor, error)
 
 func init() {
 	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) {

+ 7 - 3
pkg/tsdb/fake_test.go

@@ -1,6 +1,10 @@
 package tsdb
 
-import "context"
+import (
+	"context"
+
+	"github.com/grafana/grafana/pkg/models"
+)
 
 type FakeExecutor struct {
 	results   map[string]*QueryResult
@@ -9,11 +13,11 @@ type FakeExecutor struct {
 
 type ResultsFn func(context *QueryContext) *QueryResult
 
-func NewFakeExecutor(dsInfo *DataSourceInfo) *FakeExecutor {
+func NewFakeExecutor(dsInfo *models.DataSource) (*FakeExecutor, error) {
 	return &FakeExecutor{
 		results:   make(map[string]*QueryResult),
 		resultsFn: make(map[string]ResultsFn),
-	}
+	}, nil
 }
 
 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"
 
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
 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 (
-	glog       log.Logger
-	HttpClient *http.Client
+	glog log.Logger
 )
 
 func init() {
 	glog = log.New("tsdb.graphite")
 	tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
-
-	HttpClient = tsdb.GetDefaultClient()
 }
 
 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
 	}
 
-	res, err := ctxhttp.Do(ctx, HttpClient, req)
+	res, err := ctxhttp.Do(ctx, e.HttpClient, req)
 	if err != nil {
 		result.Error = err
 		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"
 
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
 type InfluxDBExecutor struct {
-	*tsdb.DataSourceInfo
+	*models.DataSource
 	QueryParser    *InfluxdbQueryParser
 	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{
-		DataSourceInfo: dsInfo,
+		DataSource:     datasource,
 		QueryParser:    &InfluxdbQueryParser{},
 		ResponseParser: &ResponseParser{},
-	}
+		HttpClient:     httpClient,
+	}, nil
 }
 
 var (
-	glog       log.Logger
-	HttpClient *http.Client
+	glog log.Logger
 )
 
 func init() {
 	glog = log.New("tsdb.influxdb")
 	tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor)
-
-	HttpClient = tsdb.GetDefaultClient()
 }
 
 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)
 	}
 
-	resp, err := ctxhttp.Do(ctx, HttpClient, req)
+	resp, err := ctxhttp.Do(ctx, e.HttpClient, req)
 	if err != nil {
 		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) {
 	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 {
 			return nil, err
 		}

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

@@ -4,12 +4,12 @@ import (
 	"strconv"
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/tsdb"
+	"github.com/grafana/grafana/pkg/models"
 )
 
 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")
 	rawQuery := model.Get("query").MustString("")
 	useRawQuery := model.Get("rawQuery").MustBool(false)

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

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

+ 2 - 15
pkg/tsdb/models.go

@@ -2,6 +2,7 @@ package tsdb
 
 import (
 	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
 	"gopkg.in/guregu/null.v3"
 )
 
@@ -9,7 +10,7 @@ type Query struct {
 	RefId         string
 	Model         *simplejson.Json
 	Depends       []string
-	DataSource    *DataSourceInfo
+	DataSource    *models.DataSource
 	Results       []*TimeSeries
 	Exclude       bool
 	MaxDataPoints int64
@@ -28,20 +29,6 @@ type Response struct {
 	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 {
 	TimeElapsed int64
 }

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

@@ -17,28 +17,36 @@ import (
 	"gopkg.in/guregu/null.v3"
 
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
 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 (
-	plog       log.Logger
-	HttpClient *http.Client
+	plog log.Logger
 )
 
 func init() {
 	plog = log.New("tsdb.opentsdb")
 	tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor)
-
-	HttpClient = tsdb.GetDefaultClient()
 }
 
 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
 	}
 
-	res, err := ctxhttp.Do(ctx, HttpClient, req)
+	res, err := ctxhttp.Do(ctx, e.httpClient, req)
 	if err != nil {
 		result.Error = err
 		return result

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

@@ -9,18 +9,30 @@ import (
 
 	"gopkg.in/guregu/null.v3"
 
+	"net/http"
+
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/prometheus/client_golang/api/prometheus"
 	pmodel "github.com/prometheus/common/model"
 )
 
 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 (
@@ -36,7 +48,8 @@ func init() {
 
 func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
 	cfg := prometheus.Config{
-		Address: e.DataSourceInfo.Url,
+		Address:   e.DataSource.Url,
+		Transport: e.Transport,
 	}
 
 	client, err := prometheus.New(cfg)

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

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

+ 19 - 18
pkg/tsdb/tsdb_test.go

@@ -5,6 +5,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
@@ -15,9 +16,9 @@ func TestMetricQuery(t *testing.T) {
 		Convey("Given 3 queries for 2 data sources", func() {
 			request := &Request{
 				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() {
 			request := &Request{
 				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() {
 		req := &Request{
 			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() {
 		req := &Request{
 			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() {
 		req := &Request{
 			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() {
 		req := &Request{
 			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{
 			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 {
-	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

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

@@ -74,7 +74,9 @@ export class BackendSrv {
     return this.$http(options).then(results => {
       if (options.method !== 'GET') {
         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;

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

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

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

@@ -89,6 +89,7 @@ export class KeybindingSrv {
 
     this.bind('mod+o', () => {
       dashboard.sharedCrosshair = !dashboard.sharedCrosshair;
+      appEvents.emit('graph-hover-clear');
       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 "webhook": return "fa fa-cubes";
       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">
         <span class="gf-form-label width-12">Type</span>
         <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>
         </div>
       </div>
@@ -122,6 +122,23 @@
       </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-inline">
         <div class="gf-form width-6">

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

@@ -123,11 +123,11 @@
       </div>
 
       <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 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>
         <a class="btn btn-link" ng-click="dismiss()">Cancel</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 class="gf-form"
                         label="Hide Controls"
-                        tooltip="Hide row controls. Shortcut: CTRL+H"
+                        tooltip="Hide row controls. Shortcut: CTRL+H or CMD+H"
                         checked="dashboard.hideControls"
                         label-class="width-11">
         </gf-form-switch>
         <gf-form-switch class="gf-form"
-                        label="Shared Crosshair"
-                        tooltip="Shared Crosshair line on all graphs. Shortcut: CTRL+O"
+                        label="Shared Tooltip"
+                        tooltip="Shared Tooltip on all graphs. Shortcut: CTRL+O or CMD+O"
                         checked="dashboard.sharedCrosshair"
                         label-class="width-11">
         </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) {
     this.row = this.rowCtrl.row;
     this.dashboard = this.rowCtrl.dashboard;
-    this.allPanels = _.orderBy(_.map(config.panels, item => item), 'sort');
-    this.panelHits = this.allPanels;
     this.activeIndex = 0;
+
+    this.allPanels = _.chain(config.panels)
+      .filter({hideFromList: false})
+      .map(item => item)
+      .orderBy('sort')
+      .value();
+
+    this.panelHits = this.allPanels;
   }
 
   keyDown(evt) {
@@ -78,11 +84,8 @@ export class AddPanelCtrl {
     var panel = {
       id: null,
       title: config.new_panel_title,
-      error: false,
       span: span < defaultSpan && span > 0 ? span : defaultSpan,
-      editable: true,
       type: panelPluginInfo.id,
-      isNew: true,
     };
 
     this.rowCtrl.closeDropView();

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

@@ -28,6 +28,7 @@ export class DataSourceEditCtrl {
   tabIndex: number;
   hasDashboards: boolean;
   editForm: any;
+  gettingStarted: boolean;
 
   /** @ngInject */
   constructor(
@@ -46,12 +47,23 @@ export class DataSourceEditCtrl {
         if (this.$routeParams.id) {
           this.getDatasourceById(this.$routeParams.id);
         } 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() {
       if (datasourceTypes.length > 0) {
         this.types = datasourceTypes;

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

@@ -15,8 +15,8 @@
 	</div>
 
 	<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>
 		</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>
 			<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
 		</div>
-
 		<div class="gf-form offset-width-7">
 			<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>
 		</div>
-
 		<div class="gf-form offset-width-7">
 			<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>
 		</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 ng-if="agg.type === 'filters'">
@@ -94,5 +101,3 @@
 	</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'>
 	</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'">
 		<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>

+ 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;
   };
 
@@ -65,6 +69,10 @@ function (queryDef) {
       esAgg.interval = "$interval";
     }
 
+    if (settings.missing) {
+      esAgg.missing = settings.missing;
+    }
+
     return esAgg;
   };
 

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

@@ -72,7 +72,8 @@ function (_) {
     pipelineOptions: {
       'moving_avg' : [
         {text: 'window', default: 5},
-        {text: 'model', default: 'simple'}
+        {text: 'model', default: 'simple'},
+        {text: 'predict', default: 0}
       ],
       'derivative': [
         {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.events';
 
-import angular from 'angular';
 import $ from 'jquery';
-import moment from 'moment';
 import _ from 'lodash';
+import moment from 'moment';
 import kbn from   'app/core/utils/kbn';
+import {appEvents, coreModule} from 'app/core/core';
 import GraphTooltip from './graph_tooltip';
 import {ThresholdManager} from './threshold_manager';
 
-var module = angular.module('grafana.directives');
 var labelWidthCache = {};
 
-module.directive('grafanaGraph', function($rootScope, timeSrv) {
+coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
   return {
     restrict: 'A',
     template: '',
@@ -28,14 +27,19 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
       var ctrl = scope.ctrl;
       var dashboard = ctrl.dashboard;
       var panel = ctrl.panel;
-      var data, annotations;
+      var data;
+      var annotations;
+      var plot;
       var sortedSeries;
       var legendSideLastValue = null;
       var rootScope = scope.$root;
       var panelWidth = 0;
       var thresholdManager = new ThresholdManager(ctrl);
-      var plot;
+      var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
+        return sortedSeries;
+      });
 
+      // panel events
       ctrl.events.on('panel-teardown', () => {
         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;
         }
+        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);
 
-      // 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) {
         if (!panel.legend.show || panel.legend.rightSide) {
@@ -272,7 +277,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
             color: '#666'
           },
           crosshair: {
-            mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null
+            mode: 'x'
           }
         };
 
@@ -565,10 +570,6 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
         return "%H:%M";
       }
 
-      var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
-        return sortedSeries;
-      });
-
       elem.bind("plotselected", function (event, ranges) {
         scope.$apply(function() {
           timeSrv.setTime({

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

@@ -1,16 +1,18 @@
 define([
   'jquery',
-  'lodash'
+  'app/core/core',
 ],
-function ($) {
+function ($, core) {
   'use strict';
 
+  var appEvents = core.appEvents;
+
   function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
     var self = this;
     var ctrl = scope.ctrl;
     var panel = ctrl.panel;
 
-    var $tooltip = $('<div id="tooltip" class="graph-tooltip">');
+    var $tooltip = $('<div class="graph-tooltip">');
 
     this.destroy = function() {
       $tooltip.remove();
@@ -41,7 +43,7 @@ function ($) {
       return j - 1;
     };
 
-    this.showTooltip = function(absoluteTime, innerHtml, pos, xMode) {
+    this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
       if (xMode === 'time') {
         innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml;
       }
@@ -140,22 +142,43 @@ function ($) {
           plot.unhighlight();
         }
       }
-
-      if (dashboard.sharedCrosshair) {
-        ctrl.publishAppEvent('clearCrosshair');
-      }
+      appEvents.emit('graph-hover-clear');
     });
 
     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 plotData = plot.getData();
       var xAxes = plot.getXAxes();
       var xMode = xAxes[0].options.mode;
       var seriesList = getSeriesFn();
+      var allSeriesMode = panel.tooltip.shared;
       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) {
@@ -168,7 +191,7 @@ function ($) {
         tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
       }
 
-      if (panel.tooltip.shared) {
+      if (allSeriesMode) {
         plot.unhighlight();
 
         var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
@@ -211,7 +234,7 @@ function ($) {
           plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
         }
 
-        self.showTooltip(absoluteTime, seriesHtml, pos, xMode);
+        self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
       }
       // single series tooltip
       else if (item) {
@@ -232,13 +255,13 @@ function ($) {
 
         group += '<div class="graph-tooltip-value">' + value + '</div>';
 
-        self.showTooltip(absoluteTime, group, pos, xMode);
+        self.renderAndShow(absoluteTime, group, pos, xMode);
       }
       // no hit
       else {
         $tooltip.detach();
       }
-    });
+    };
   }
 
   return GraphTooltip;

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

@@ -88,7 +88,7 @@ class GraphCtrl extends MetricsPanelCtrl {
       avg: false
     },
     // how null points should be handled
-    nullPointMode : 'connected',
+    nullPointMode : 'null',
     // staircase line mode
     steppedLine: false,
     // tooltip options
@@ -96,7 +96,6 @@ class GraphCtrl extends MetricsPanelCtrl {
       value_type: 'individual',
       shared: true,
       sort: 0,
-      msResolution: false,
     },
     // time overrides
     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() {
 
-  beforeEach(angularMocks.module('grafana.directives'));
+  beforeEach(angularMocks.module('grafana.core'));
 
   function graphScenario(desc, func, elementWidth = 500)  {
     describe(desc, function() {

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

@@ -48,7 +48,7 @@
 			</div>
 		</div>
 		<div class="section gf-form-group">
-			<h5 class="section-heading">Hover info</h5>
+			<h5 class="section-heading">Hover tooltip</h5>
 			<div class="gf-form">
 				<label class="gf-form-label width-9">Mode</label>
 				<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;
 
   // Set and populate defaults
-  panelDefaults = {
-  };
+  panelDefaults = {};
 
   /** @ngInject */
   constructor($scope, $injector, private backendSrv, private $location) {

+ 2 - 2
public/dashboards/home.json

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

+ 1 - 0
public/sass/_grafana.scss

@@ -43,6 +43,7 @@
 @import "components/submenu";
 @import "components/panel_alertlist";
 @import "components/panel_dashlist";
+@import "components/panel_gettingstarted";
 @import "components/panel_pluginlist";
 @import "components/panel_singlestat";
 @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;
     color: $link-color;
     position: relative;
-    top: 2px;
+    top: 1px;
   }
 }