Merge branch 'master' into elastic5_support

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

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

@ -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

@ -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

@ -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

@ -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

@ -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

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

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

@ -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

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

@ -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,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

@ -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-----`

@ -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 {

@ -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 {

@ -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),
}
}

@ -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,

@ -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",
},
})
}

@ -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})
}

@ -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")

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

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

@ -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-----`

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

@ -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 {

@ -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:"-"`

@ -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,
},
},
}

@ -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))
})
})
}

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

@ -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")
})
})
})
}

@ -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",
}))
}

@ -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
})
}

@ -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 {

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

@ -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 {

@ -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

@ -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,
}
}

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

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

@ -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,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
}

@ -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

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

@ -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() {

@ -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

@ -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;

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

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

@ -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";
}
}

@ -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">

@ -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>

@ -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>

@ -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();

@ -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;

@ -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>

@ -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>

@ -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>

@ -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;
};

@ -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},

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

@ -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>

@ -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>

After

Width:  |  Height:  |  Size: 8.8 KiB

@ -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>

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

@ -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"
}
}
}

@ -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({

@ -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;

@ -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,

@ -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() {

@ -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">

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

@ -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,

@ -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";

@ -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;
}
}
}

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

Loading…
Cancel
Save