mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into ui-new-red-green-blue
This commit is contained in:
commit
a4dd63e224
@ -11,6 +11,7 @@
|
|||||||
* **MSSQL**: Timerange are now passed for template variable queries [#13324](https://github.com/grafana/grafana/issues/13324), thx [@thatsparesh](https://github.com/thatsparesh)
|
* **MSSQL**: Timerange are now passed for template variable queries [#13324](https://github.com/grafana/grafana/issues/13324), thx [@thatsparesh](https://github.com/thatsparesh)
|
||||||
* **Annotations**: Support PATCH verb in annotations http api [#12546](https://github.com/grafana/grafana/issues/12546), thx [@SamuelToh](https://github.com/SamuelToh)
|
* **Annotations**: Support PATCH verb in annotations http api [#12546](https://github.com/grafana/grafana/issues/12546), thx [@SamuelToh](https://github.com/SamuelToh)
|
||||||
* **Templating**: Add json formatting to variable interpolation [#15291](https://github.com/grafana/grafana/issues/15291), thx [@mtanda](https://github.com/mtanda)
|
* **Templating**: Add json formatting to variable interpolation [#15291](https://github.com/grafana/grafana/issues/15291), thx [@mtanda](https://github.com/mtanda)
|
||||||
|
* **Login**: Anonymous usage stats for token auth [#15288](https://github.com/grafana/grafana/issues/15288)
|
||||||
|
|
||||||
### 6.0.0-beta1 fixes
|
### 6.0.0-beta1 fixes
|
||||||
|
|
||||||
|
13
README.md
13
README.md
@ -7,13 +7,18 @@
|
|||||||
Grafana is an open source, feature rich metrics dashboard and graph editor for
|
Grafana is an open source, feature rich metrics dashboard and graph editor for
|
||||||
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Join us Feb 25-26 in Los Angeles, California for GrafanaCon - a two-day event with talks focused on Grafana and the surrounding open source monitoring ecosystem. Get deep dives into Loki, the Explore workflow and all of the new features of Grafana 6, plus participate in hands on workshops to help you get the most out of your data.
|
||||||
|
|
||||||
|
Time is running out - grab your ticket now! http://grafanacon.org
|
||||||
|
|
||||||
|
<!---
|
||||||

|

|
||||||
|
-->
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get)
|
Head to [docs.grafana.org](http://docs.grafana.org/installation/) for documentation or [download](https://grafana.com/get) to get the latest release.
|
||||||
the latest release.
|
|
||||||
|
|
||||||
If you have any problems please read the [troubleshooting guide](http://docs.grafana.org/installation/troubleshooting/).
|
|
||||||
|
|
||||||
## Documentation & Support
|
## Documentation & Support
|
||||||
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
|
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"@types/react-dom": "^16.0.9",
|
"@types/react-dom": "^16.0.9",
|
||||||
"@types/react-grid-layout": "^0.16.6",
|
"@types/react-grid-layout": "^0.16.6",
|
||||||
"@types/react-select": "^2.0.4",
|
"@types/react-select": "^2.0.4",
|
||||||
|
"@types/react-transition-group": "^2.0.15",
|
||||||
"@types/react-virtualized": "^9.18.12",
|
"@types/react-virtualized": "^9.18.12",
|
||||||
"angular-mocks": "1.6.6",
|
"angular-mocks": "1.6.6",
|
||||||
"autoprefixer": "^6.4.0",
|
"autoprefixer": "^6.4.0",
|
||||||
|
54
pkg/infra/usagestats/service.go
Normal file
54
pkg/infra/usagestats/service.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package usagestats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/social"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
var metricsLogger log.Logger = log.New("metrics")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.RegisterService(&UsageStatsService{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsageStatsService struct {
|
||||||
|
Cfg *setting.Cfg `inject:""`
|
||||||
|
Bus bus.Bus `inject:""`
|
||||||
|
SQLStore *sqlstore.SqlStore `inject:""`
|
||||||
|
|
||||||
|
oauthProviders map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uss *UsageStatsService) Init() error {
|
||||||
|
|
||||||
|
uss.oauthProviders = social.GetOAuthProviders(uss.Cfg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uss *UsageStatsService) Run(ctx context.Context) error {
|
||||||
|
uss.updateTotalStats()
|
||||||
|
|
||||||
|
onceEveryDayTick := time.NewTicker(time.Hour * 24)
|
||||||
|
everyMinuteTicker := time.NewTicker(time.Minute)
|
||||||
|
defer onceEveryDayTick.Stop()
|
||||||
|
defer everyMinuteTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-onceEveryDayTick.C:
|
||||||
|
uss.sendUsageStats(uss.oauthProviders)
|
||||||
|
case <-everyMinuteTicker.C:
|
||||||
|
uss.updateTotalStats()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
177
pkg/infra/usagestats/usage_stats.go
Normal file
177
pkg/infra/usagestats/usage_stats.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
package usagestats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
|
||||||
|
|
||||||
|
func (uss *UsageStatsService) sendUsageStats(oauthProviders map[string]bool) {
|
||||||
|
if !setting.ReportingEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsLogger.Debug(fmt.Sprintf("Sending anonymous usage stats to %s", usageStatsURL))
|
||||||
|
|
||||||
|
version := strings.Replace(setting.BuildVersion, ".", "_", -1)
|
||||||
|
|
||||||
|
metrics := map[string]interface{}{}
|
||||||
|
report := map[string]interface{}{
|
||||||
|
"version": version,
|
||||||
|
"metrics": metrics,
|
||||||
|
"os": runtime.GOOS,
|
||||||
|
"arch": runtime.GOARCH,
|
||||||
|
"edition": getEdition(),
|
||||||
|
"packaging": setting.Packaging,
|
||||||
|
}
|
||||||
|
|
||||||
|
statsQuery := models.GetSystemStatsQuery{}
|
||||||
|
if err := uss.Bus.Dispatch(&statsQuery); err != nil {
|
||||||
|
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
|
||||||
|
metrics["stats.users.count"] = statsQuery.Result.Users
|
||||||
|
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
|
||||||
|
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
|
||||||
|
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
|
||||||
|
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
|
||||||
|
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
|
||||||
|
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
|
||||||
|
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
|
||||||
|
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
|
||||||
|
metrics["stats.stars.count"] = statsQuery.Result.Stars
|
||||||
|
metrics["stats.folders.count"] = statsQuery.Result.Folders
|
||||||
|
metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
|
||||||
|
metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
|
||||||
|
metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
|
||||||
|
metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
|
||||||
|
metrics["stats.teams.count"] = statsQuery.Result.Teams
|
||||||
|
metrics["stats.total_sessions.count"] = statsQuery.Result.Sessions
|
||||||
|
|
||||||
|
userCount := statsQuery.Result.Users
|
||||||
|
avgSessionsPerUser := statsQuery.Result.Sessions
|
||||||
|
if userCount != 0 {
|
||||||
|
avgSessionsPerUser = avgSessionsPerUser / userCount
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics["stats.avg_sessions_per_user.count"] = avgSessionsPerUser
|
||||||
|
|
||||||
|
dsStats := models.GetDataSourceStatsQuery{}
|
||||||
|
if err := uss.Bus.Dispatch(&dsStats); err != nil {
|
||||||
|
metricsLogger.Error("Failed to get datasource stats", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send counters for each data source
|
||||||
|
// but ignore any custom data sources
|
||||||
|
// as sending that name could be sensitive information
|
||||||
|
dsOtherCount := 0
|
||||||
|
for _, dsStat := range dsStats.Result {
|
||||||
|
if models.IsKnownDataSourcePlugin(dsStat.Type) {
|
||||||
|
metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
|
||||||
|
} else {
|
||||||
|
dsOtherCount += dsStat.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metrics["stats.ds.other.count"] = dsOtherCount
|
||||||
|
|
||||||
|
metrics["stats.packaging."+setting.Packaging+".count"] = 1
|
||||||
|
|
||||||
|
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
|
||||||
|
if err := uss.Bus.Dispatch(&dsAccessStats); err != nil {
|
||||||
|
metricsLogger.Error("Failed to get datasource access stats", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send access counters for each data source
|
||||||
|
// but ignore any custom data sources
|
||||||
|
// as sending that name could be sensitive information
|
||||||
|
dsAccessOtherCount := make(map[string]int64)
|
||||||
|
for _, dsAccessStat := range dsAccessStats.Result {
|
||||||
|
if dsAccessStat.Access == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
access := strings.ToLower(dsAccessStat.Access)
|
||||||
|
|
||||||
|
if models.IsKnownDataSourcePlugin(dsAccessStat.Type) {
|
||||||
|
metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
|
||||||
|
} else {
|
||||||
|
old := dsAccessOtherCount[access]
|
||||||
|
dsAccessOtherCount[access] = old + dsAccessStat.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for access, count := range dsAccessOtherCount {
|
||||||
|
metrics["stats.ds_access.other."+access+".count"] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
anStats := models.GetAlertNotifierUsageStatsQuery{}
|
||||||
|
if err := uss.Bus.Dispatch(&anStats); err != nil {
|
||||||
|
metricsLogger.Error("Failed to get alert notification stats", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stats := range anStats.Result {
|
||||||
|
metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
|
||||||
|
}
|
||||||
|
|
||||||
|
authTypes := map[string]bool{}
|
||||||
|
authTypes["anonymous"] = setting.AnonymousEnabled
|
||||||
|
authTypes["basic_auth"] = setting.BasicAuthEnabled
|
||||||
|
authTypes["ldap"] = setting.LdapEnabled
|
||||||
|
authTypes["auth_proxy"] = setting.AuthProxyEnabled
|
||||||
|
|
||||||
|
for provider, enabled := range oauthProviders {
|
||||||
|
authTypes["oauth_"+provider] = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
for authType, enabled := range authTypes {
|
||||||
|
enabledValue := 0
|
||||||
|
if enabled {
|
||||||
|
enabledValue = 1
|
||||||
|
}
|
||||||
|
metrics["stats.auth_enabled."+authType+".count"] = enabledValue
|
||||||
|
}
|
||||||
|
|
||||||
|
out, _ := json.MarshalIndent(report, "", " ")
|
||||||
|
data := bytes.NewBuffer(out)
|
||||||
|
|
||||||
|
client := http.Client{Timeout: 5 * time.Second}
|
||||||
|
go client.Post(usageStatsURL, "application/json", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uss *UsageStatsService) updateTotalStats() {
|
||||||
|
statsQuery := models.GetSystemStatsQuery{}
|
||||||
|
if err := uss.Bus.Dispatch(&statsQuery); err != nil {
|
||||||
|
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
|
||||||
|
metrics.M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
|
||||||
|
metrics.M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
|
||||||
|
metrics.M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
|
||||||
|
metrics.M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEdition() string {
|
||||||
|
if setting.IsEnterprise {
|
||||||
|
return "enterprise"
|
||||||
|
} else {
|
||||||
|
return "oss"
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package metrics
|
package usagestats
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -15,14 +15,21 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMetrics(t *testing.T) {
|
func TestMetrics(t *testing.T) {
|
||||||
Convey("Test send usage stats", t, func() {
|
Convey("Test send usage stats", t, func() {
|
||||||
|
uss := &UsageStatsService{
|
||||||
|
Bus: bus.New(),
|
||||||
|
SQLStore: sqlstore.InitTestDB(t),
|
||||||
|
}
|
||||||
|
|
||||||
var getSystemStatsQuery *models.GetSystemStatsQuery
|
var getSystemStatsQuery *models.GetSystemStatsQuery
|
||||||
bus.AddHandler("test", func(query *models.GetSystemStatsQuery) error {
|
uss.Bus.AddHandler(func(query *models.GetSystemStatsQuery) error {
|
||||||
|
|
||||||
query.Result = &models.SystemStats{
|
query.Result = &models.SystemStats{
|
||||||
Dashboards: 1,
|
Dashboards: 1,
|
||||||
Datasources: 2,
|
Datasources: 2,
|
||||||
@ -38,13 +45,14 @@ func TestMetrics(t *testing.T) {
|
|||||||
ProvisionedDashboards: 12,
|
ProvisionedDashboards: 12,
|
||||||
Snapshots: 13,
|
Snapshots: 13,
|
||||||
Teams: 14,
|
Teams: 14,
|
||||||
|
Sessions: 15,
|
||||||
}
|
}
|
||||||
getSystemStatsQuery = query
|
getSystemStatsQuery = query
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
var getDataSourceStatsQuery *models.GetDataSourceStatsQuery
|
var getDataSourceStatsQuery *models.GetDataSourceStatsQuery
|
||||||
bus.AddHandler("test", func(query *models.GetDataSourceStatsQuery) error {
|
uss.Bus.AddHandler(func(query *models.GetDataSourceStatsQuery) error {
|
||||||
query.Result = []*models.DataSourceStats{
|
query.Result = []*models.DataSourceStats{
|
||||||
{
|
{
|
||||||
Type: models.DS_ES,
|
Type: models.DS_ES,
|
||||||
@ -68,7 +76,7 @@ func TestMetrics(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
var getDataSourceAccessStatsQuery *models.GetDataSourceAccessStatsQuery
|
var getDataSourceAccessStatsQuery *models.GetDataSourceAccessStatsQuery
|
||||||
bus.AddHandler("test", func(query *models.GetDataSourceAccessStatsQuery) error {
|
uss.Bus.AddHandler(func(query *models.GetDataSourceAccessStatsQuery) error {
|
||||||
query.Result = []*models.DataSourceAccessStats{
|
query.Result = []*models.DataSourceAccessStats{
|
||||||
{
|
{
|
||||||
Type: models.DS_ES,
|
Type: models.DS_ES,
|
||||||
@ -116,7 +124,7 @@ func TestMetrics(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery
|
var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery
|
||||||
bus.AddHandler("test", func(query *models.GetAlertNotifierUsageStatsQuery) error {
|
uss.Bus.AddHandler(func(query *models.GetAlertNotifierUsageStatsQuery) error {
|
||||||
query.Result = []*models.NotifierUsageStats{
|
query.Result = []*models.NotifierUsageStats{
|
||||||
{
|
{
|
||||||
Type: "slack",
|
Type: "slack",
|
||||||
@ -155,11 +163,11 @@ func TestMetrics(t *testing.T) {
|
|||||||
"grafana_com": true,
|
"grafana_com": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUsageStats(oauthProviders)
|
uss.sendUsageStats(oauthProviders)
|
||||||
|
|
||||||
Convey("Given reporting not enabled and sending usage stats", func() {
|
Convey("Given reporting not enabled and sending usage stats", func() {
|
||||||
setting.ReportingEnabled = false
|
setting.ReportingEnabled = false
|
||||||
sendUsageStats(oauthProviders)
|
uss.sendUsageStats(oauthProviders)
|
||||||
|
|
||||||
Convey("Should not gather stats or call http endpoint", func() {
|
Convey("Should not gather stats or call http endpoint", func() {
|
||||||
So(getSystemStatsQuery, ShouldBeNil)
|
So(getSystemStatsQuery, ShouldBeNil)
|
||||||
@ -179,7 +187,7 @@ func TestMetrics(t *testing.T) {
|
|||||||
setting.Packaging = "deb"
|
setting.Packaging = "deb"
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
sendUsageStats(oauthProviders)
|
uss.sendUsageStats(oauthProviders)
|
||||||
|
|
||||||
Convey("Should gather stats and call http endpoint", func() {
|
Convey("Should gather stats and call http endpoint", func() {
|
||||||
if waitTimeout(&wg, 2*time.Second) {
|
if waitTimeout(&wg, 2*time.Second) {
|
||||||
@ -221,6 +229,8 @@ func TestMetrics(t *testing.T) {
|
|||||||
So(metrics.Get("stats.provisioned_dashboards.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.ProvisionedDashboards)
|
So(metrics.Get("stats.provisioned_dashboards.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.ProvisionedDashboards)
|
||||||
So(metrics.Get("stats.snapshots.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Snapshots)
|
So(metrics.Get("stats.snapshots.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Snapshots)
|
||||||
So(metrics.Get("stats.teams.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Teams)
|
So(metrics.Get("stats.teams.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Teams)
|
||||||
|
So(metrics.Get("stats.total_sessions.count").MustInt64(), ShouldEqual, 15)
|
||||||
|
So(metrics.Get("stats.avg_sessions_per_user.count").MustInt64(), ShouldEqual, 5)
|
||||||
|
|
||||||
So(metrics.Get("stats.ds."+models.DS_ES+".count").MustInt(), ShouldEqual, 9)
|
So(metrics.Get("stats.ds."+models.DS_ES+".count").MustInt(), ShouldEqual, 9)
|
||||||
So(metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt(), ShouldEqual, 10)
|
So(metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt(), ShouldEqual, 10)
|
||||||
@ -246,6 +256,7 @@ func TestMetrics(t *testing.T) {
|
|||||||
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
|
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
|
||||||
|
|
||||||
So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
|
So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -1,17 +1,8 @@
|
|||||||
package metrics
|
package metrics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -68,23 +59,6 @@ var (
|
|||||||
grafanaBuildVersion *prometheus.GaugeVec
|
grafanaBuildVersion *prometheus.GaugeVec
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
|
|
||||||
counter := prometheus.NewCounterVec(opts, labels)
|
|
||||||
|
|
||||||
for _, label := range labelValues {
|
|
||||||
counter.WithLabelValues(label).Add(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return counter
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
|
|
||||||
counter := prometheus.NewCounter(opts)
|
|
||||||
counter.Add(0)
|
|
||||||
|
|
||||||
return counter
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
M_Instance_Start = prometheus.NewCounter(prometheus.CounterOpts{
|
M_Instance_Start = prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
Name: "instance_start_total",
|
Name: "instance_start_total",
|
||||||
@ -362,154 +336,19 @@ func initMetricVars() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTotalStats() {
|
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
|
||||||
statsQuery := models.GetSystemStatsQuery{}
|
counter := prometheus.NewCounterVec(opts, labels)
|
||||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
|
||||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
for _, label := range labelValues {
|
||||||
return
|
counter.WithLabelValues(label).Add(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
|
return counter
|
||||||
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
|
|
||||||
M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
|
|
||||||
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
|
|
||||||
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
|
func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
|
||||||
|
counter := prometheus.NewCounter(opts)
|
||||||
|
counter.Add(0)
|
||||||
|
|
||||||
func getEdition() string {
|
return counter
|
||||||
if setting.IsEnterprise {
|
|
||||||
return "enterprise"
|
|
||||||
} else {
|
|
||||||
return "oss"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendUsageStats(oauthProviders map[string]bool) {
|
|
||||||
if !setting.ReportingEnabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
metricsLogger.Debug("Sending anonymous usage stats to stats.grafana.org")
|
|
||||||
|
|
||||||
version := strings.Replace(setting.BuildVersion, ".", "_", -1)
|
|
||||||
|
|
||||||
metrics := map[string]interface{}{}
|
|
||||||
report := map[string]interface{}{
|
|
||||||
"version": version,
|
|
||||||
"metrics": metrics,
|
|
||||||
"os": runtime.GOOS,
|
|
||||||
"arch": runtime.GOARCH,
|
|
||||||
"edition": getEdition(),
|
|
||||||
"packaging": setting.Packaging,
|
|
||||||
}
|
|
||||||
|
|
||||||
statsQuery := models.GetSystemStatsQuery{}
|
|
||||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
|
||||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
|
|
||||||
metrics["stats.users.count"] = statsQuery.Result.Users
|
|
||||||
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
|
|
||||||
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
|
|
||||||
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
|
|
||||||
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
|
|
||||||
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
|
|
||||||
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
|
|
||||||
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
|
|
||||||
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
|
|
||||||
metrics["stats.stars.count"] = statsQuery.Result.Stars
|
|
||||||
metrics["stats.folders.count"] = statsQuery.Result.Folders
|
|
||||||
metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
|
|
||||||
metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
|
|
||||||
metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
|
|
||||||
metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
|
|
||||||
metrics["stats.teams.count"] = statsQuery.Result.Teams
|
|
||||||
|
|
||||||
dsStats := models.GetDataSourceStatsQuery{}
|
|
||||||
if err := bus.Dispatch(&dsStats); err != nil {
|
|
||||||
metricsLogger.Error("Failed to get datasource stats", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// send counters for each data source
|
|
||||||
// but ignore any custom data sources
|
|
||||||
// as sending that name could be sensitive information
|
|
||||||
dsOtherCount := 0
|
|
||||||
for _, dsStat := range dsStats.Result {
|
|
||||||
if models.IsKnownDataSourcePlugin(dsStat.Type) {
|
|
||||||
metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
|
|
||||||
} else {
|
|
||||||
dsOtherCount += dsStat.Count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
metrics["stats.ds.other.count"] = dsOtherCount
|
|
||||||
|
|
||||||
metrics["stats.packaging."+setting.Packaging+".count"] = 1
|
|
||||||
|
|
||||||
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
|
|
||||||
if err := bus.Dispatch(&dsAccessStats); err != nil {
|
|
||||||
metricsLogger.Error("Failed to get datasource access stats", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// send access counters for each data source
|
|
||||||
// but ignore any custom data sources
|
|
||||||
// as sending that name could be sensitive information
|
|
||||||
dsAccessOtherCount := make(map[string]int64)
|
|
||||||
for _, dsAccessStat := range dsAccessStats.Result {
|
|
||||||
if dsAccessStat.Access == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
access := strings.ToLower(dsAccessStat.Access)
|
|
||||||
|
|
||||||
if models.IsKnownDataSourcePlugin(dsAccessStat.Type) {
|
|
||||||
metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
|
|
||||||
} else {
|
|
||||||
old := dsAccessOtherCount[access]
|
|
||||||
dsAccessOtherCount[access] = old + dsAccessStat.Count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for access, count := range dsAccessOtherCount {
|
|
||||||
metrics["stats.ds_access.other."+access+".count"] = count
|
|
||||||
}
|
|
||||||
|
|
||||||
anStats := models.GetAlertNotifierUsageStatsQuery{}
|
|
||||||
if err := bus.Dispatch(&anStats); err != nil {
|
|
||||||
metricsLogger.Error("Failed to get alert notification stats", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, stats := range anStats.Result {
|
|
||||||
metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
|
|
||||||
}
|
|
||||||
|
|
||||||
authTypes := map[string]bool{}
|
|
||||||
authTypes["anonymous"] = setting.AnonymousEnabled
|
|
||||||
authTypes["basic_auth"] = setting.BasicAuthEnabled
|
|
||||||
authTypes["ldap"] = setting.LdapEnabled
|
|
||||||
authTypes["auth_proxy"] = setting.AuthProxyEnabled
|
|
||||||
|
|
||||||
for provider, enabled := range oauthProviders {
|
|
||||||
authTypes["oauth_"+provider] = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
for authType, enabled := range authTypes {
|
|
||||||
enabledValue := 0
|
|
||||||
if enabled {
|
|
||||||
enabledValue = 1
|
|
||||||
}
|
|
||||||
metrics["stats.auth_enabled."+authType+".count"] = enabledValue
|
|
||||||
}
|
|
||||||
|
|
||||||
out, _ := json.MarshalIndent(report, "", " ")
|
|
||||||
data := bytes.NewBuffer(out)
|
|
||||||
|
|
||||||
client := http.Client{Timeout: 5 * time.Second}
|
|
||||||
go client.Post(usageStatsURL, "application/json", data)
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package metrics
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
||||||
@ -30,7 +29,6 @@ type InternalMetricsService struct {
|
|||||||
|
|
||||||
intervalSeconds int64
|
intervalSeconds int64
|
||||||
graphiteCfg *graphitebridge.Config
|
graphiteCfg *graphitebridge.Config
|
||||||
oauthProviders map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (im *InternalMetricsService) Init() error {
|
func (im *InternalMetricsService) Init() error {
|
||||||
@ -50,22 +48,6 @@ func (im *InternalMetricsService) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
M_Instance_Start.Inc()
|
M_Instance_Start.Inc()
|
||||||
|
|
||||||
// set the total stats gauges before we publishing metrics
|
<-ctx.Done()
|
||||||
updateTotalStats()
|
return ctx.Err()
|
||||||
|
|
||||||
onceEveryDayTick := time.NewTicker(time.Hour * 24)
|
|
||||||
everyMinuteTicker := time.NewTicker(time.Minute)
|
|
||||||
defer onceEveryDayTick.Stop()
|
|
||||||
defer everyMinuteTicker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-onceEveryDayTick.C:
|
|
||||||
sendUsageStats(im.oauthProviders)
|
|
||||||
case <-everyMinuteTicker.C:
|
|
||||||
updateTotalStats()
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/social"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@ -24,8 +22,6 @@ func (im *InternalMetricsService) readSettings() error {
|
|||||||
return fmt.Errorf("Unable to parse metrics graphite section, %v", err)
|
return fmt.Errorf("Unable to parse metrics graphite section, %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
im.oauthProviders = social.GetOAuthProviders(im.Cfg)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ type SystemStats struct {
|
|||||||
FolderPermissions int64
|
FolderPermissions int64
|
||||||
Folders int64
|
Folders int64
|
||||||
ProvisionedDashboards int64
|
ProvisionedDashboards int64
|
||||||
|
Sessions int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataSourceStats struct {
|
type DataSourceStats struct {
|
||||||
|
@ -74,7 +74,8 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
|
|||||||
|
|
||||||
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`)
|
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`)
|
||||||
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`)
|
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`)
|
||||||
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams`)
|
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams,`)
|
||||||
|
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("user_auth_token") + `) AS sessions`)
|
||||||
|
|
||||||
var stats m.SystemStats
|
var stats m.SystemStats
|
||||||
_, err := x.SQL(sb.GetSqlString(), sb.params...).Get(&stats)
|
_, err := x.SQL(sb.GetSqlString(), sb.params...).Get(&stats)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import Transition from 'react-transition-group/Transition';
|
import Transition, { ExitHandler } from 'react-transition-group/Transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
duration: number;
|
duration: number;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
in: boolean;
|
in: boolean;
|
||||||
unmountOnExit?: boolean;
|
unmountOnExit?: boolean;
|
||||||
|
onExited?: ExitHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FadeIn: FC<Props> = props => {
|
export const FadeIn: FC<Props> = props => {
|
||||||
@ -22,7 +23,12 @@ export const FadeIn: FC<Props> = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition in={props.in} timeout={props.duration} unmountOnExit={props.unmountOnExit || false}>
|
<Transition
|
||||||
|
in={props.in}
|
||||||
|
timeout={props.duration}
|
||||||
|
unmountOnExit={props.unmountOnExit || false}
|
||||||
|
onExited={props.onExited}
|
||||||
|
>
|
||||||
{state => (
|
{state => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -8,6 +8,16 @@ jest.mock('../../app_events', () => ({
|
|||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('app/store/store', () => ({
|
||||||
|
store: {
|
||||||
|
getState: jest.fn().mockReturnValue({
|
||||||
|
location: {
|
||||||
|
lastUpdated: 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
contextSrv: {
|
contextSrv: {
|
||||||
sidemenu: true,
|
sidemenu: true,
|
||||||
|
@ -3,9 +3,16 @@ import appEvents from '../../app_events';
|
|||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import TopSection from './TopSection';
|
import TopSection from './TopSection';
|
||||||
import BottomSection from './BottomSection';
|
import BottomSection from './BottomSection';
|
||||||
|
import { store } from 'app/store/store';
|
||||||
|
|
||||||
export class SideMenu extends PureComponent {
|
export class SideMenu extends PureComponent {
|
||||||
toggleSideMenu = () => {
|
toggleSideMenu = () => {
|
||||||
|
// ignore if we just made a location change, stops hiding sidemenu on double clicks of back button
|
||||||
|
const timeSinceLocationChanged = new Date().getTime() - store.getState().location.lastUpdated;
|
||||||
|
if (timeSinceLocationChanged < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
contextSrv.toggleSideMenu();
|
contextSrv.toggleSideMenu();
|
||||||
appEvents.emit('toggle-sidemenu');
|
appEvents.emit('toggle-sidemenu');
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import './directives/dash_class';
|
|
||||||
import './directives/dropdown_typeahead';
|
import './directives/dropdown_typeahead';
|
||||||
import './directives/autofill_event_fix';
|
import './directives/autofill_event_fix';
|
||||||
import './directives/metric_segment';
|
import './directives/metric_segment';
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import $ from 'jquery';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import coreModule from '../core_module';
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
function dashClass($timeout) {
|
|
||||||
return {
|
|
||||||
link: ($scope, elem) => {
|
|
||||||
const body = $('body');
|
|
||||||
|
|
||||||
$scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
|
|
||||||
console.log('view-mode-changed', panel.fullscreen);
|
|
||||||
if (panel.fullscreen) {
|
|
||||||
body.addClass('panel-in-fullscreen');
|
|
||||||
} else {
|
|
||||||
$timeout(() => {
|
|
||||||
body.removeClass('panel-in-fullscreen');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
body.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
|
|
||||||
|
|
||||||
$scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
|
|
||||||
if (newValue) {
|
|
||||||
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
|
|
||||||
setTimeout(() => {
|
|
||||||
elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
|
|
||||||
}, 10);
|
|
||||||
} else {
|
|
||||||
elem.removeClass('dashboard-page--settings-opening');
|
|
||||||
elem.removeClass('dashboard-page--settings-open');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.directive('dashClass', dashClass);
|
|
@ -340,6 +340,11 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
|
|||||||
return a[1] - b[1];
|
return a[1] - b[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
return { datapoints: series.datapoints, target: series.alias, color: series.color };
|
return {
|
||||||
|
datapoints: series.datapoints,
|
||||||
|
target: series.alias,
|
||||||
|
alias: series.alias,
|
||||||
|
color: series.color
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ export const initialState: LocationState = {
|
|||||||
query: {},
|
query: {},
|
||||||
routeParams: {},
|
routeParams: {},
|
||||||
replace: false,
|
replace: false,
|
||||||
|
lastUpdated: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
||||||
@ -28,6 +29,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
|
|||||||
query: { ...query },
|
query: { ...query },
|
||||||
routeParams: routeParams || state.routeParams,
|
routeParams: routeParams || state.routeParams,
|
||||||
replace: replace === true,
|
replace: replace === true,
|
||||||
|
lastUpdated: new Date().getTime(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,13 @@ import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { DashNavButton } from './DashNavButton';
|
import { DashNavButton } from './DashNavButton';
|
||||||
|
import { Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
import { updateLocation } from 'app/core/actions';
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
@ -33,7 +34,6 @@ export class DashNav extends PureComponent<Props> {
|
|||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.playlistSrv = this.props.$injector.get('playlistSrv');
|
this.playlistSrv = this.props.$injector.get('playlistSrv');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,26 +123,54 @@ export class DashNav extends PureComponent<Props> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
renderDashboardTitleSearchButton() {
|
||||||
const { dashboard, isFullscreen, editview, onAddPanel } = this.props;
|
const { dashboard } = this.props;
|
||||||
const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta;
|
|
||||||
const { snapshot } = dashboard;
|
|
||||||
|
|
||||||
|
const folderTitle = dashboard.meta.folderTitle;
|
||||||
const haveFolder = dashboard.meta.folderId > 0;
|
const haveFolder = dashboard.meta.folderId > 0;
|
||||||
const snapshotUrl = snapshot && snapshot.originalUrl;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar">
|
<>
|
||||||
<div>
|
<div>
|
||||||
<a className="navbar-page-btn" onClick={this.onOpenSearch}>
|
<a className="navbar-page-btn" onClick={this.onOpenSearch}>
|
||||||
<i className="gicon gicon-dashboard" />
|
{!this.isInFullscreenOrSettings && <i className="gicon gicon-dashboard" />}
|
||||||
{haveFolder && <span className="navbar-page-btn--folder">{folderTitle} / </span>}
|
{haveFolder && <span className="navbar-page-btn--folder">{folderTitle} / </span>}
|
||||||
{dashboard.title}
|
{dashboard.title}
|
||||||
<i className="fa fa-caret-down" />
|
<i className="fa fa-caret-down" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="navbar__spacer" />
|
<div className="navbar__spacer" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isInFullscreenOrSettings() {
|
||||||
|
return this.props.editview || this.props.isFullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBackButton() {
|
||||||
|
return (
|
||||||
|
<div className="navbar-edit">
|
||||||
|
<Tooltip content="Go back (Esc)">
|
||||||
|
<button className="navbar-edit__back-btn" onClick={this.onClose}>
|
||||||
|
<i className="fa fa-arrow-left" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { dashboard, onAddPanel } = this.props;
|
||||||
|
const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
|
||||||
|
const { snapshot } = dashboard;
|
||||||
|
|
||||||
|
const snapshotUrl = snapshot && snapshot.originalUrl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="navbar">
|
||||||
|
{this.isInFullscreenOrSettings && this.renderBackButton()}
|
||||||
|
{this.renderDashboardTitleSearchButton()}
|
||||||
|
|
||||||
{this.playlistSrv.isPlaying && (
|
{this.playlistSrv.isPlaying && (
|
||||||
<div className="navbar-buttons navbar-buttons--playlist">
|
<div className="navbar-buttons navbar-buttons--playlist">
|
||||||
@ -228,17 +256,6 @@ export class DashNav extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
|
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
|
||||||
|
|
||||||
{(isFullscreen || editview) && (
|
|
||||||
<div className="navbar-buttons navbar-buttons--close">
|
|
||||||
<DashNavButton
|
|
||||||
tooltip="Back to dashboard"
|
|
||||||
classSuffix="primary"
|
|
||||||
icon="fa fa-reply"
|
|
||||||
onClick={this.onClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -149,6 +149,10 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearQuery = () => {
|
||||||
|
this.setState({ searchQuery: '' });
|
||||||
|
};
|
||||||
|
|
||||||
onPanelOptionsChanged = (options: any) => {
|
onPanelOptionsChanged = (options: any) => {
|
||||||
this.props.panel.updateOptions(options);
|
this.props.panel.updateOptions(options);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
@ -241,7 +245,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
|||||||
setScrollTop={this.setScrollTop}
|
setScrollTop={this.setScrollTop}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
|
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}>
|
||||||
<VizTypePicker
|
<VizTypePicker
|
||||||
current={plugin}
|
current={plugin}
|
||||||
onTypeChanged={this.onTypeChanged}
|
onTypeChanged={this.onTypeChanged}
|
||||||
|
@ -220,6 +220,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
||||||
{supportsLogs && (
|
{supportsLogs && (
|
||||||
<LogsContainer
|
<LogsContainer
|
||||||
|
width={width}
|
||||||
exploreId={exploreId}
|
exploreId={exploreId}
|
||||||
onChangeTime={this.onChangeTime}
|
onChangeTime={this.onChangeTime}
|
||||||
onClickLabel={this.onClickLabel}
|
onClickLabel={this.onClickLabel}
|
||||||
|
@ -51,6 +51,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data?: LogsModel;
|
data?: LogsModel;
|
||||||
|
width: number;
|
||||||
exploreId: string;
|
exploreId: string;
|
||||||
highlighterExpressions: string[];
|
highlighterExpressions: string[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@ -165,6 +166,7 @@ export default class Logs extends PureComponent<Props, State> {
|
|||||||
range,
|
range,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
scanRange,
|
||||||
|
width,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -215,6 +217,7 @@ export default class Logs extends PureComponent<Props, State> {
|
|||||||
<Graph
|
<Graph
|
||||||
data={timeSeries}
|
data={timeSeries}
|
||||||
height={100}
|
height={100}
|
||||||
|
width={width}
|
||||||
range={range}
|
range={range}
|
||||||
id={`explore-logs-graph-${exploreId}`}
|
id={`explore-logs-graph-${exploreId}`}
|
||||||
onChangeTime={this.props.onChangeTime}
|
onChangeTime={this.props.onChangeTime}
|
||||||
|
@ -25,6 +25,7 @@ interface LogsContainerProps {
|
|||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
showingLogs: boolean;
|
showingLogs: boolean;
|
||||||
toggleLogs: typeof toggleLogs;
|
toggleLogs: typeof toggleLogs;
|
||||||
|
width: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||||
@ -46,6 +47,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
showingLogs,
|
showingLogs,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
scanRange,
|
||||||
|
width,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -63,6 +65,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
range={range}
|
range={range}
|
||||||
scanning={scanning}
|
scanning={scanning}
|
||||||
scanRange={scanRange}
|
scanRange={scanRange}
|
||||||
|
width={width}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
|
@ -2,61 +2,65 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { Select, SelectOptionItem } from '@grafana/ui';
|
// import { Select, SelectOptionItem } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { QueryEditorProps } from '@grafana/ui/src/types';
|
import { QueryEditorProps } from '@grafana/ui/src/types';
|
||||||
import { LokiDatasource } from '../datasource';
|
import { LokiDatasource } from '../datasource';
|
||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
import { LokiQueryField } from './LokiQueryField';
|
// import { LokiQueryField } from './LokiQueryField';
|
||||||
|
|
||||||
type Props = QueryEditorProps<LokiDatasource, LokiQuery>;
|
type Props = QueryEditorProps<LokiDatasource, LokiQuery>;
|
||||||
|
|
||||||
interface State {
|
// interface State {
|
||||||
query: LokiQuery;
|
// query: LokiQuery;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export class LokiQueryEditor extends PureComponent<Props> {
|
export class LokiQueryEditor extends PureComponent<Props> {
|
||||||
state: State = {
|
// state: State = {
|
||||||
query: this.props.query,
|
// query: this.props.query,
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
onRunQuery = () => {
|
// onRunQuery = () => {
|
||||||
const { query } = this.state;
|
// const { query } = this.state;
|
||||||
|
//
|
||||||
this.props.onChange(query);
|
// this.props.onChange(query);
|
||||||
this.props.onRunQuery();
|
// this.props.onRunQuery();
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
onFieldChange = (query: LokiQuery, override?) => {
|
// onFieldChange = (query: LokiQuery, override?) => {
|
||||||
this.setState({
|
// this.setState({
|
||||||
query: {
|
// query: {
|
||||||
...this.state.query,
|
// ...this.state.query,
|
||||||
expr: query.expr,
|
// expr: query.expr,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
onFormatChanged = (option: SelectOptionItem) => {
|
// onFormatChanged = (option: SelectOptionItem) => {
|
||||||
this.props.onChange({
|
// this.props.onChange({
|
||||||
...this.state.query,
|
// ...this.state.query,
|
||||||
resultFormat: option.value,
|
// resultFormat: option.value,
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { query } = this.state;
|
// const { query } = this.state;
|
||||||
const { datasource } = this.props;
|
// const { datasource } = this.props;
|
||||||
const formatOptions: SelectOptionItem[] = [
|
// const formatOptions: SelectOptionItem[] = [
|
||||||
{ label: 'Time Series', value: 'time_series' },
|
// { label: 'Time Series', value: 'time_series' },
|
||||||
{ label: 'Table', value: 'table' },
|
// { label: 'Table', value: 'table' },
|
||||||
];
|
// ];
|
||||||
|
//
|
||||||
query.resultFormat = query.resultFormat || 'time_series';
|
// query.resultFormat = query.resultFormat || 'time_series';
|
||||||
const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
|
// const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<div className="gf-form">
|
||||||
|
<div className="gf-form-label">Loki is currently not supported as dashboard data source. We are working on it!</div>
|
||||||
|
</div>
|
||||||
|
{/*
|
||||||
<LokiQueryField
|
<LokiQueryField
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
query={query}
|
query={query}
|
||||||
@ -78,6 +82,7 @@ export class LokiQueryEditor extends PureComponent<Props> {
|
|||||||
<div className="gf-form-label gf-form-label--grow" />
|
<div className="gf-form-label gf-form-label--grow" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -150,8 +150,8 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
})
|
})
|
||||||
.when('/dashboards/f/:uid', {
|
.when('/dashboards/f/:uid', {
|
||||||
templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
|
templateUrl: 'public/app/features/folders/partials/folder_dashboards.html',
|
||||||
controller: 'FolderDashboardsCtrl',
|
controller: FolderDashboardsCtrl,
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
})
|
})
|
||||||
.when('/explore', {
|
.when('/explore', {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { createLogger } from 'redux-logger';
|
// import { createLogger } from 'redux-logger';
|
||||||
import sharedReducers from 'app/core/reducers';
|
import sharedReducers from 'app/core/reducers';
|
||||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||||
import teamsReducers from 'app/features/teams/state/reducers';
|
import teamsReducers from 'app/features/teams/state/reducers';
|
||||||
@ -41,7 +41,7 @@ export function configureStore() {
|
|||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
// DEV builds we had the logger middleware
|
// DEV builds we had the logger middleware
|
||||||
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
|
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
|
||||||
} else {
|
} else {
|
||||||
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
|
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ export interface LocationState {
|
|||||||
query: UrlQueryMap;
|
query: UrlQueryMap;
|
||||||
routeParams: UrlQueryMap;
|
routeParams: UrlQueryMap;
|
||||||
replace: boolean;
|
replace: boolean;
|
||||||
|
lastUpdated: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
|
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.navbar {
|
.navbar {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 40px;
|
padding-left: 20px;
|
||||||
z-index: $zindex-navbar-fixed;
|
z-index: $zindex-navbar-fixed;
|
||||||
height: $navbarHeight;
|
height: $navbarHeight;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
@ -41,15 +41,12 @@
|
|||||||
|
|
||||||
.panel-in-fullscreen {
|
.panel-in-fullscreen {
|
||||||
.navbar {
|
.navbar {
|
||||||
padding-left: 15px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-button--add-panel,
|
.navbar-button--add-panel,
|
||||||
.navbar-button--star,
|
.navbar-button--star,
|
||||||
.navbar-button--tv,
|
.navbar-button--tv,
|
||||||
.navbar-page-btn .fa-caret-down {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-buttons--close {
|
.navbar-buttons--close {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -179,3 +176,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-edit {
|
||||||
|
display: flex;
|
||||||
|
height: $navbarHeight;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-edit__back-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid $text-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
transition: transform 0.1s ease 0.1s;
|
||||||
|
color: $text-color;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $text-color-strong;
|
||||||
|
border-color: $text-color-strong;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,6 +86,10 @@
|
|||||||
.panel-editor-container__panel {
|
.panel-editor-container__panel {
|
||||||
margin: 0 $dashboard-padding;
|
margin: 0 $dashboard-padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-editor-container__resizer {
|
.panel-editor-container__resizer {
|
||||||
|
@ -21,9 +21,9 @@
|
|||||||
// Search
|
// Search
|
||||||
.search-field-wrapper {
|
.search-field-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: $navbarHeight;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: $navbarBackground;
|
background-color: $navbarBackground;
|
||||||
box-shadow: $navbarShadow;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
& > input {
|
& > input {
|
||||||
|
Loading…
Reference in New Issue
Block a user