diff --git a/CHANGELOG.md b/CHANGELOG.md index 5398481b198..bd7c3fad139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,27 @@ # 4.1-beta (unreleased) +### 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) +* **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) -### 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) -* **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) - -# 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) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index dfe84f142ec..1411bcf756f 100644 --- a/docs/sources/alerting/notifications.md +++ b/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 diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md index 340d97438ae..72c995adedf 100644 --- a/docs/sources/http_api/org.md +++ b/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 diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index ab51e0ac124..903e03228ed 100644 --- a/docs/sources/installation/debian.md +++ b/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 diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 370bac44cf2..8ccaa43bc79 100644 --- a/docs/sources/installation/rpm.md +++ b/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 diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index fc1495ec311..5217181f1ee 100644 --- a/docs/sources/installation/windows.md +++ b/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 diff --git a/docs/sources/reference/export_import.md b/docs/sources/reference/export_import.md index 7430f4891a6..bc05795c030 100644 --- a/docs/sources/reference/export_import.md +++ b/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). diff --git a/docs/sources/reference/singlestat.md b/docs/sources/reference/singlestat.md index 1fb0c254c01..ed0a956b7eb 100644 --- a/docs/sources/reference/singlestat.md +++ b/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] diff --git a/latest.json b/latest.json index dd8e0ce33da..78fd48769e8 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "4.0.1", - "testing": "4.0.1" + "stable": "4.0.2", + "testing": "4.0.2" } diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index 7619dd2e0f4..57adcbd3481 100755 --- a/packaging/publish/publish.sh +++ b/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 diff --git a/pkg/api/api.go b/pkg/api/api.go index 4fa28f799b0..90e0e1bdfa8 100644 --- a/pkg/api/api.go +++ b/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)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 0ed77fe982e..55925c4faf6 100644 --- a/pkg/api/dashboard.go +++ b/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") diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 39de6911008..db4c5166feb 100644 --- a/pkg/api/dataproxy.go +++ b/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 diff --git a/pkg/api/dataproxy_test.go b/pkg/api/dataproxy_test.go index 50abf317e8a..f3ed6994cff 100644 --- a/pkg/api/dataproxy_test.go +++ b/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-----` diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index cc305221817..f0c022c1622 100644 --- a/pkg/api/datasources.go +++ b/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 { diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 17ac39fa344..e9efd1b7c01 100644 --- a/pkg/api/dtos/models.go +++ b/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 { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 384dd7f0ea1..a3dd9f9ce61 100644 --- a/pkg/api/frontendsettings.go +++ b/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), } } diff --git a/pkg/api/index.go b/pkg/api/index.go index 99a5f78f9c9..5bc4344a8ba 100644 --- a/pkg/api/index.go +++ b/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, diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 5516bdd52a5..dad3b159ad6 100644 --- a/pkg/api/metrics.go +++ b/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", }, }) } diff --git a/pkg/api/user.go b/pkg/api/user.go index f98eec02c40..715103aacbd 100644 --- a/pkg/api/user.go +++ b/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}) +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 8eb4eb4baea..3c6c362f949 100644 --- a/pkg/metrics/metrics.go +++ b/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") diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index a546d7e76fc..7a64656b0ee 100644 --- a/pkg/middleware/middleware.go +++ b/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 } diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go new file mode 100644 index 00000000000..e32c0ac9e7c --- /dev/null +++ b/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 +} diff --git a/pkg/models/datasource_cache_test.go b/pkg/models/datasource_cache_test.go new file mode 100644 index 00000000000..25fee55529d --- /dev/null +++ b/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-----` diff --git a/pkg/models/helpflags.go b/pkg/models/helpflags.go new file mode 100644 index 00000000000..1bab730e265 --- /dev/null +++ b/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 +} diff --git a/pkg/models/user.go b/pkg/models/user.go index 1f99f866c86..8bfad7c2fb4 100644 --- a/pkg/models/user.go +++ b/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 { diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index ca60662ade8..7de12b4036c 100644 --- a/pkg/plugins/models.go +++ b/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:"-"` diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index e58dbf1d583..d7e0a0c29e3 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/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, }, }, } diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index badf868e8fc..47a13a5a33d 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/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)) + }) }) } diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go new file mode 100644 index 00000000000..e16cda4bc5b --- /dev/null +++ b/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 +} diff --git a/pkg/services/alerting/notifiers/opsgenie_test.go b/pkg/services/alerting/notifiers/opsgenie_test.go new file mode 100644 index 00000000000..9dcb9f3c600 --- /dev/null +++ b/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") + }) + }) + }) +} diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index 51db4d74857..67446f64d1a 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/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", + })) } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index bb21995f54a..b26a9153f55 100644 --- a/pkg/services/sqlstore/user.go +++ b/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 + }) +} diff --git a/pkg/tsdb/batch.go b/pkg/tsdb/batch.go index 284a158bf5f..8130ac94b0d 100644 --- a/pkg/tsdb/batch.go +++ b/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 { diff --git a/pkg/tsdb/executor.go b/pkg/tsdb/executor.go index cc1d592dcd5..251b3dc947a 100644 --- a/pkg/tsdb/executor.go +++ b/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) { diff --git a/pkg/tsdb/fake_test.go b/pkg/tsdb/fake_test.go index c403fdba4fb..3c773971240 100644 --- a/pkg/tsdb/fake_test.go +++ b/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 { diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 3d6af5beb5b..a467e839b82 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/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 diff --git a/pkg/tsdb/http.go b/pkg/tsdb/http.go deleted file mode 100644 index f5de146d470..00000000000 --- a/pkg/tsdb/http.go +++ /dev/null @@ -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, - } -} diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 658fbcff023..21d359d24bf 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/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 } diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index 410bf8f6e82..f3d87739e5b 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/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) diff --git a/pkg/tsdb/influxdb/model_parser_test.go b/pkg/tsdb/influxdb/model_parser_test.go index eae6e163cc0..f7049efb9a7 100644 --- a/pkg/tsdb/influxdb/model_parser_test.go +++ b/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(), } diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 366709cfba7..e95713e7077 100644 --- a/pkg/tsdb/models.go +++ b/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 } diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index c5e5b0020d1..f5b6ffda7d2 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/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 diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 21e1dfdef24..c391f00920a 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/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) diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go index cf2dcc0f898..6aefd8686d8 100644 --- a/pkg/tsdb/testdata/testdata.go +++ b/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() { diff --git a/pkg/tsdb/tsdb_test.go b/pkg/tsdb/tsdb_test.go index 998f59a6b9d..2b1a2372cd6 100644 --- a/pkg/tsdb/tsdb_test.go +++ b/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 diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 1e620e88216..9a5ec5d219b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/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; diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index ac00528db20..3d048e4f869 100644 --- a/public/app/core/services/context_srv.ts +++ b/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) { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index c1109fa2b2e..2b509db822a 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/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(); }); diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 426ff62382f..c6683556745 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/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"; } } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index ac4afbf56fc..b5076efd792 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -19,7 +19,7 @@
Type
-
@@ -122,6 +122,23 @@ +
+

OpsGenie settings

+
+ API Key + +
+
+ + +
+
+
diff --git a/public/app/features/dashboard/import/dash_import.html b/public/app/features/dashboard/import/dash_import.html index 3e914245627..5e19a02c28c 100644 --- a/public/app/features/dashboard/import/dash_import.html +++ b/public/app/features/dashboard/import/dash_import.html @@ -123,11 +123,11 @@
- - Cancel Back diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index 79cc7d12157..efa174acdb9 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -57,13 +57,13 @@ diff --git a/public/app/features/dashboard/row/add_panel.ts b/public/app/features/dashboard/row/add_panel.ts index 771f5e00b64..1ea0cc5159e 100644 --- a/public/app/features/dashboard/row/add_panel.ts +++ b/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(); diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 9c808b188c4..597f879e980 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/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; diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index c4df2b6adf9..c7cc6fb440d 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -15,8 +15,8 @@
-
- +
+ ADD ROW
diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index 3790d18857e..e3e6cd6f5ff 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -57,16 +57,23 @@
-
-
+
+ + +
@@ -94,5 +101,3 @@
- - diff --git a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html index faa12b5693d..74cf23516cd 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html @@ -53,6 +53,11 @@
+
+ + +
+
diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js index f5f831efae8..8b00d092080 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.js +++ b/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; }; diff --git a/public/app/plugins/datasource/elasticsearch/query_def.js b/public/app/plugins/datasource/elasticsearch/query_def.js index 187e5c2f4fa..9fbfb3495d6 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.js +++ b/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}, diff --git a/public/app/plugins/panel/gettingstarted/README.md b/public/app/plugins/panel/gettingstarted/README.md new file mode 100644 index 00000000000..463769dad1f --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/README.md @@ -0,0 +1,2 @@ +# Plugin List Panel - Native Plugin + diff --git a/public/app/plugins/panel/gettingstarted/editor.html b/public/app/plugins/panel/gettingstarted/editor.html new file mode 100644 index 00000000000..c0577578598 --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/editor.html @@ -0,0 +1,40 @@ +
+
+
+ Mode +
+ +
+
+
+ + + +
+
+ +
+
+ Search options + Query + + + +
+ +
+ Tags + + + +
+
+ +
+
+ Limit number to + +
+
+
diff --git a/public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg b/public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg new file mode 100644 index 00000000000..8bac231bedf --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/gettingstarted/module.html b/public/app/plugins/panel/gettingstarted/module.html new file mode 100644 index 00000000000..71aeb07dd9a --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/module.html @@ -0,0 +1,19 @@ +
+
+
+ Getting Started with Grafana + +
+ +
+
diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts new file mode 100644 index 00000000000..9e19cce1063 --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -0,0 +1,119 @@ +/// + +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} diff --git a/public/app/plugins/panel/gettingstarted/plugin.json b/public/app/plugins/panel/gettingstarted/plugin.json new file mode 100644 index 00000000000..106f509281f --- /dev/null +++ b/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" + } + } +} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index d1997acc508..41154fa00d3 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/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,26 +49,6 @@ 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) { - return; - } - - if (dashboard.sharedCrosshair) { - if (plot) { - plot.setCrosshair({ x: info.pos.x, y: info.pos.y }); - } - } - }, scope); - - rootScope.onAppEvent('clearCrosshair', function() { - if (plot) { - plot.clearCrosshair(); - } - }, scope); - - // Receive render events ctrl.events.on('render', function(renderData) { data = renderData || data; if (!data) { @@ -74,6 +58,27 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { render_panel(); }); + // global events + appEvents.on('graph-hover', function(evt) { + // ignore other graph hover events if shared tooltip is disabled + if (!dashboard.sharedCrosshair) { + return; + } + + // ignore if we are the emitter + if (!plot || evt.panel.id === panel.id || ctrl.otherPanelInFullscreenMode()) { + return; + } + + tooltip.show(evt.pos); + }, scope); + + appEvents.on('graph-hover-clear', function(event, info) { + if (plot) { + tooltip.clear(plot); + } + }, scope); + function getLegendHeight(panelHeight) { if (!panel.legend.show || panel.legend.rightSide) { return 0; @@ -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({ diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 5ef6c65a400..4705d5d5a13 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/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 = $('
'); + var $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 = '
'+ absoluteTime + '
' + 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 += '
' + value + '
'; - self.showTooltip(absoluteTime, group, pos, xMode); + self.renderAndShow(absoluteTime, group, pos, xMode); } // no hit else { $tooltip.detach(); } - }); + }; } return GraphTooltip; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index e546a33c30b..d86212d33e6 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/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, diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 046409b77b2..5578b3a361e 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/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() { diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index 6845181b8b2..8ace16c6078 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -48,7 +48,7 @@
-
Hover info
+
Hover tooltip
diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index 5ef1fb6fb0c..8a47061feb0 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/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) { diff --git a/public/dashboards/home.json b/public/dashboards/home.json index 393cbc5865c..d2a41a63c43 100644 --- a/public/dashboards/home.json +++ b/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, diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 53a96cf9291..25db63858e8 100644 --- a/public/sass/_grafana.scss +++ b/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"; diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss new file mode 100644 index 00000000000..d5a231a220c --- /dev/null +++ b/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; + } + } +} diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index 08467126ba2..2426f70afee 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -65,6 +65,6 @@ border-bottom: 2px solid $panel-bg; color: $link-color; position: relative; - top: 2px; + top: 1px; } }