From 10d706dccff98ff5504bd49094bf5004de88c365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 18 Oct 2018 14:34:25 +0200 Subject: [PATCH 001/116] wip: enterprise docs --- docs/sources/enterprise/index.md | 11 +++++++++++ docs/sources/whatsnew/index.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/sources/enterprise/index.md diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md new file mode 100644 index 00000000000..a241d84b50e --- /dev/null +++ b/docs/sources/enterprise/index.md @@ -0,0 +1,11 @@ ++++ +title = "Grafana Enterprise" +description = "Grafana Enterprise overview" +type = "docs" +[menu.docs] +name = "Enterprise" +identifier = "enterprise" +weight = 4 ++++ + +### Grafana Enterprise diff --git a/docs/sources/whatsnew/index.md b/docs/sources/whatsnew/index.md index df472f07093..f4159643d72 100644 --- a/docs/sources/whatsnew/index.md +++ b/docs/sources/whatsnew/index.md @@ -3,7 +3,7 @@ title = "What's New in Grafana" [menu.docs] name = "What's New In Grafana" identifier = "whatsnew" -weight = 3 +weight = 5 +++ From 6092fa4dc3cc4d00c6b48626f8ac1ec1ac1fe5c7 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 29 Oct 2018 23:13:07 +0300 Subject: [PATCH 002/116] Fix bug with background color in table cell with link --- public/app/plugins/panel/table/renderer.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index c25e37357cb..39a2f98c165 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -211,16 +211,17 @@ export class TableRenderer { value = this.formatColumnValue(columnIndex, value); const column = this.table.columns[columnIndex]; - let style = ''; + let cellStyle = ''; + let textStyle = ''; const cellClasses = []; let cellClass = ''; if (this.colorState.cell) { - style = ' style="background-color:' + this.colorState.cell + '"'; + cellStyle = ' style="background-color:' + this.colorState.cell + '"'; cellClasses.push('table-panel-color-cell'); this.colorState.cell = null; } else if (this.colorState.value) { - style = ' style="color:' + this.colorState.value + '"'; + textStyle = ' style="color:' + this.colorState.value + '"'; this.colorState.value = null; } // because of the fixed table headers css only solution @@ -232,7 +233,7 @@ export class TableRenderer { } if (value === undefined) { - style = ' style="display:none;"'; + cellStyle = ' style="display:none;"'; column.hidden = true; } else { column.hidden = false; @@ -258,7 +259,7 @@ export class TableRenderer { cellClasses.push('table-panel-cell-link'); columnHtml += ` - + ${value} `; @@ -283,7 +284,7 @@ export class TableRenderer { cellClass = ' class="' + cellClasses.join(' ') + '"'; } - columnHtml = '' + columnHtml + ''; + columnHtml = '' + columnHtml + ''; return columnHtml; } From 355e76a48ea9956ac0126527fb9960c849af489d Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 29 Oct 2018 23:26:29 +0300 Subject: [PATCH 003/116] Fix cell coloring --- public/app/plugins/panel/table/renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 39a2f98c165..524aa06343b 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -284,7 +284,7 @@ export class TableRenderer { cellClass = ' class="' + cellClasses.join(' ') + '"'; } - columnHtml = '' + columnHtml + ''; + columnHtml = '' + columnHtml + ''; return columnHtml; } From cfb061ddaba0d8bf38199b74eb0f25eaa33b27ad Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 26 Oct 2018 10:40:33 +0200 Subject: [PATCH 004/116] refactor datasource caching --- pkg/api/dataproxy.go | 50 ++++--------------------------- pkg/api/http_server.go | 18 ++++++----- pkg/api/metrics.go | 5 +++- pkg/cmd/grafana-server/server.go | 9 +++++- pkg/middleware/headers.go | 14 +++++++++ pkg/middleware/middleware.go | 1 + pkg/models/context.go | 1 + pkg/services/cache/cache.go | 17 +++++++++++ pkg/services/datasources/cache.go | 49 ++++++++++++++++++++++++++++++ 9 files changed, 109 insertions(+), 55 deletions(-) create mode 100644 pkg/middleware/headers.go create mode 100644 pkg/services/cache/cache.go create mode 100644 pkg/services/datasources/cache.go diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 3bb2f236129..1bc97eb42ed 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -1,62 +1,22 @@ package api import ( - "fmt" - "github.com/pkg/errors" - "time" - "github.com/grafana/grafana/pkg/api/pluginproxy" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ) -const HeaderNameNoBackendCache = "X-Grafana-NoCache" - -func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) { - userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{ - User: c.SignedInUser, - } - if err := bus.Dispatch(&userPermissionsQuery); err != nil { - if err != bus.ErrHandlerNotFound { - return nil, err - } - } else { - permissionType, exists := userPermissionsQuery.Result[id] - if exists && permissionType != m.DsPermissionQuery { - return nil, errors.New("User not allowed to access datasource") - } - } - - nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" - cacheKey := fmt.Sprintf("ds-%d", id) - - if !nocache { - if cached, found := hs.cache.Get(cacheKey); found { - ds := cached.(*m.DataSource) - if ds.OrgId == c.OrgId { - return ds, nil - } - } - } - - query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId} - if err := bus.Dispatch(&query); err != nil { - return nil, err - } - - hs.cache.Set(cacheKey, query.Result, time.Second*5) - return query.Result, nil -} - func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer) dsId := c.ParamsInt64(":id") - ds, err := hs.getDatasourceFromCache(dsId, c) - + ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache) if err != nil { + if err == m.ErrDataSourceAccessDenied { + c.JsonApiErr(403, "Access denied to datasource", nil) + return + } c.JsonApiErr(500, "Unable to load datasource meta data", err) return } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 858b3c5a8c5..ce28e4716ee 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -16,7 +16,6 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" - gocache "github.com/patrickmn/go-cache" macaron "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/api/live" @@ -28,6 +27,8 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/cache" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -46,19 +47,19 @@ type HTTPServer struct { macaron *macaron.Macaron context context.Context streamManager *live.StreamManager - cache *gocache.Cache httpSrv *http.Server - RouteRegister routing.RouteRegister `inject:""` - Bus bus.Bus `inject:""` - RenderService rendering.Service `inject:""` - Cfg *setting.Cfg `inject:""` - HooksService *hooks.HooksService `inject:""` + RouteRegister routing.RouteRegister `inject:""` + Bus bus.Bus `inject:""` + RenderService rendering.Service `inject:""` + Cfg *setting.Cfg `inject:""` + HooksService *hooks.HooksService `inject:""` + CacheService *cache.CacheService `inject:""` + DatasourceCache datasources.CacheService `inject:""` } func (hs *HTTPServer) Init() error { hs.log = log.New("http.server") - hs.cache = gocache.New(5*time.Minute, 10*time.Minute) hs.streamManager = live.NewStreamManager() hs.macaron = hs.newMacaron() @@ -231,6 +232,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.Use(middleware.ValidateHostHeader(setting.Domain)) } + m.Use(middleware.HandleNoCacheHeader()) m.Use(middleware.AddDefaultResponseHeaders()) } diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index cb80bd346b8..a6cfe4d09de 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -25,8 +25,11 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R return Error(400, "Query missing datasourceId", nil) } - ds, err := hs.getDatasourceFromCache(datasourceId, c) + ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache) if err != nil { + if err == m.ErrDataSourceAccessDenied { + return Error(403, "Access denied to datasource", nil) + } return Error(500, "Unable to load datasource meta data", err) } diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 765b8ddf993..a07cb692a68 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -15,9 +15,15 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" - _ "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" + "github.com/grafana/grafana/pkg/services/cache" + "github.com/grafana/grafana/pkg/setting" + + "github.com/grafana/grafana/pkg/social" + + // self registering services + _ "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" _ "github.com/grafana/grafana/pkg/plugins" @@ -72,6 +78,7 @@ func (g *GrafanaServerImpl) Run() error { serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) serviceGraph.Provide(&inject.Object{Value: g.cfg}) serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) + serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)}) // self registered services services := registry.GetServices() diff --git a/pkg/middleware/headers.go b/pkg/middleware/headers.go new file mode 100644 index 00000000000..28c623d74b0 --- /dev/null +++ b/pkg/middleware/headers.go @@ -0,0 +1,14 @@ +package middleware + +import ( + m "github.com/grafana/grafana/pkg/models" + macaron "gopkg.in/macaron.v1" +) + +const HeaderNameNoBackendCache = "X-Grafana-NoCache" + +func HandleNoCacheHeader() macaron.Handler { + return func(ctx *m.ReqContext) { + ctx.SkipCache = ctx.Req.Header.Get(HeaderNameNoBackendCache) == "true" + } +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 7b29901c1a3..ace72d998eb 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -29,6 +29,7 @@ func GetContextHandler() macaron.Handler { Session: session.GetSession(), IsSignedIn: false, AllowAnonymous: false, + SkipCache: false, Logger: log.New("context"), } diff --git a/pkg/models/context.go b/pkg/models/context.go index 262f6550954..8ed7fa61a7d 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -20,6 +20,7 @@ type ReqContext struct { IsSignedIn bool IsRenderCall bool AllowAnonymous bool + SkipCache bool Logger log.Logger } diff --git a/pkg/services/cache/cache.go b/pkg/services/cache/cache.go new file mode 100644 index 00000000000..93b2cf76e26 --- /dev/null +++ b/pkg/services/cache/cache.go @@ -0,0 +1,17 @@ +package cache + +import ( + "time" + + gocache "github.com/patrickmn/go-cache" +) + +type CacheService struct { + *gocache.Cache +} + +func New(defaultExpiration, cleanupInterval time.Duration) *CacheService { + return &CacheService{ + Cache: gocache.New(defaultExpiration, cleanupInterval), + } +} diff --git a/pkg/services/datasources/cache.go b/pkg/services/datasources/cache.go new file mode 100644 index 00000000000..c984b4f8743 --- /dev/null +++ b/pkg/services/datasources/cache.go @@ -0,0 +1,49 @@ +package datasources + +import ( + "fmt" + "time" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/cache" +) + +type CacheService interface { + GetDatasource(datasourceID int64, user *m.SignedInUser, skipCache bool) (*m.DataSource, error) +} + +type CacheServiceImpl struct { + Bus bus.Bus `inject:""` + CacheService *cache.CacheService `inject:""` +} + +func init() { + registry.RegisterService(&CacheServiceImpl{}) +} + +func (dc *CacheServiceImpl) Init() error { + return nil +} + +func (dc *CacheServiceImpl) GetDatasource(datasourceID int64, user *m.SignedInUser, skipCache bool) (*m.DataSource, error) { + cacheKey := fmt.Sprintf("ds-%d", datasourceID) + + if !skipCache { + if cached, found := dc.CacheService.Get(cacheKey); found { + ds := cached.(*m.DataSource) + if ds.OrgId == user.OrgId { + return ds, nil + } + } + } + + query := m.GetDataSourceByIdQuery{Id: datasourceID, OrgId: user.OrgId} + if err := dc.Bus.Dispatch(&query); err != nil { + return nil, err + } + + dc.CacheService.Set(cacheKey, query.Result, time.Second*5) + return query.Result, nil +} From 9edaa3fa8c45f312bab0bb809b0b586318a647b8 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 26 Oct 2018 13:57:31 +0200 Subject: [PATCH 005/116] application lifecycle event support --- pkg/cmd/grafana-server/server.go | 18 ++++++++-------- pkg/lifecycle/lifecycle.go | 22 ++++++++++++++++++++ pkg/lifecycle/lifecycle_test.go | 35 ++++++++++++++++++++++++++++++++ pkg/login/auth.go | 10 ++++++--- pkg/social/social.go | 9 +++++++- 5 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 pkg/lifecycle/lifecycle.go create mode 100644 pkg/lifecycle/lifecycle_test.go diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index a07cb692a68..d8d021d502b 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -15,13 +15,17 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/lifecycle" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/registry" + + "golang.org/x/sync/errgroup" + + "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/social" - // self registering services _ "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/metrics" @@ -35,8 +39,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/rendering" _ "github.com/grafana/grafana/pkg/services/search" _ "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/social" // self registering services + "github.com/grafana/grafana/pkg/setting" // self registering services _ "github.com/grafana/grafana/pkg/tracing" "golang.org/x/sync/errgroup" ) @@ -71,8 +74,7 @@ func (g *GrafanaServerImpl) Run() error { g.loadConfiguration() g.writePIDFile() - login.Init() - social.NewOAuthService() + lifecycle.Notify(lifecycle.ApplicationStarting) serviceGraph := inject.Graph{} serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) @@ -145,7 +147,7 @@ func (g *GrafanaServerImpl) Run() error { } sendSystemdNotification("READY=1") - + lifecycle.Notify(lifecycle.ApplicationStarted) return g.childRoutines.Wait() } diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go new file mode 100644 index 00000000000..eea733a7da8 --- /dev/null +++ b/pkg/lifecycle/lifecycle.go @@ -0,0 +1,22 @@ +package lifecycle + +type Event int + +const ( + ApplicationStarting Event = iota + ApplicationStarted +) + +type EventHandlerFunc func() + +var listeners = map[int][]EventHandlerFunc{} + +func AddListener(evt Event, fn EventHandlerFunc) { + listeners[int(evt)] = append(listeners[int(evt)], fn) +} + +func Notify(evt Event) { + for _, handler := range listeners[int(evt)] { + handler() + } +} diff --git a/pkg/lifecycle/lifecycle_test.go b/pkg/lifecycle/lifecycle_test.go new file mode 100644 index 00000000000..e946cc2f4a8 --- /dev/null +++ b/pkg/lifecycle/lifecycle_test.go @@ -0,0 +1,35 @@ +package lifecycle + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestLifecycle(t *testing.T) { + Convey("TestLifecycle", t, func() { + Convey("Given listeners", func() { + applicationStartingCounter := 0 + AddListener(ApplicationStarting, func() { + applicationStartingCounter++ + }) + + applicationStartedCounter := 0 + AddListener(ApplicationStarted, func() { + applicationStartedCounter++ + }) + + Convey("When notify application starting should call listener", func() { + Notify(ApplicationStarting) + So(applicationStartingCounter, ShouldEqual, 1) + So(applicationStartedCounter, ShouldEqual, 0) + }) + + Convey("When notify application started should call listener", func() { + Notify(ApplicationStarted) + So(applicationStartingCounter, ShouldEqual, 0) + So(applicationStartedCounter, ShouldEqual, 1) + }) + }) + }) +} diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 991fa72fd54..b195b8dd2e4 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -2,7 +2,9 @@ package login import ( "errors" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/lifecycle" m "github.com/grafana/grafana/pkg/models" ) @@ -18,9 +20,11 @@ var ( ErrGettingUserQuota = errors.New("Error getting user quota") ) -func Init() { - bus.AddHandler("auth", AuthenticateUser) - loadLdapConfig() +func init() { + lifecycle.AddListener(lifecycle.ApplicationStarting, func() { + bus.AddHandler("auth", AuthenticateUser) + loadLdapConfig() + }) } func AuthenticateUser(query *m.LoginUserQuery) error { diff --git a/pkg/social/social.go b/pkg/social/social.go index 8918507f3b9..054420fd994 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -8,11 +8,18 @@ import ( "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/lifecycle" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) +func init() { + lifecycle.AddListener(lifecycle.ApplicationStarting, func() { + initOAuthService() + }) +} + type BasicUserInfo struct { Id string Name string @@ -56,7 +63,7 @@ var ( allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", grafanaCom} ) -func NewOAuthService() { +func initOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) From 70ddf936887869d3991f3926f1864a83a967d12c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 25 Oct 2018 15:20:01 +0200 Subject: [PATCH 006/116] include teams on signed in user --- pkg/models/user.go | 1 + pkg/services/sqlstore/user.go | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/pkg/models/user.go b/pkg/models/user.go index d5b912e0a9c..e3c7b556d35 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -165,6 +165,7 @@ type SignedInUser struct { IsAnonymous bool HelpFlags1 HelpFlags1 LastSeenAt time.Time + Teams []int64 } func (u *SignedInUser) ShouldUpdateLastSeenAt() bool { diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 72d5654a777..3744a04d88a 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -387,6 +387,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { if user.OrgRole == "" { user.OrgId = -1 user.OrgName = "Org missing" + } else { + getTeamsByUserQuery := &m.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId} + err = GetTeamsByUser(getTeamsByUserQuery) + if err != nil { + return err + } + + user.Teams = make([]int64, len(getTeamsByUserQuery.Result)) + for i, t := range getTeamsByUserQuery.Result { + user.Teams[i] = t.Id + } } query.Result = &user From 52d825f5351ed6271bde1e2aee27ceb4888a6689 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 26 Oct 2018 18:34:10 +0200 Subject: [PATCH 007/116] log error on datasource access denied --- pkg/api/dataproxy.go | 2 +- pkg/api/metrics.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 1bc97eb42ed..5cde0efd0b4 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -14,7 +14,7 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache) if err != nil { if err == m.ErrDataSourceAccessDenied { - c.JsonApiErr(403, "Access denied to datasource", nil) + c.JsonApiErr(403, "Access denied to datasource", err) return } c.JsonApiErr(500, "Unable to load datasource meta data", err) diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index a6cfe4d09de..6e5ae0f8761 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -28,7 +28,7 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache) if err != nil { if err == m.ErrDataSourceAccessDenied { - return Error(403, "Access denied to datasource", nil) + return Error(403, "Access denied to datasource", err) } return Error(500, "Unable to load datasource meta data", err) } From 2332b3e20579de39c7bbc227f69c11d184d6722b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 26 Oct 2018 18:55:16 +0200 Subject: [PATCH 008/116] remove unused code --- pkg/cmd/grafana-server/server.go | 5 ----- pkg/models/datasource.go | 5 ----- 2 files changed, 10 deletions(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index d8d021d502b..d4223df19ed 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -21,7 +21,6 @@ import ( "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/setting" @@ -29,9 +28,7 @@ import ( // self registering services _ "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" _ "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/registry" _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" _ "github.com/grafana/grafana/pkg/services/notifications" @@ -39,9 +36,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/rendering" _ "github.com/grafana/grafana/pkg/services/search" _ "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" // self registering services _ "github.com/grafana/grafana/pkg/tracing" - "golang.org/x/sync/errgroup" ) func NewGrafanaServer() *GrafanaServerImpl { diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index b71d17ec0d1..89439420d7a 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -207,11 +207,6 @@ func (p DsPermissionType) String() string { return names[int(p)] } -type GetDataSourcePermissionsForUserQuery struct { - User *SignedInUser - Result map[int64]DsPermissionType -} - type DatasourcesPermissionFilterQuery struct { User *SignedInUser Datasources []*DataSource From 5d4dc18bbc7586de466feb0a935a12f575907e73 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Oct 2018 12:31:28 +0100 Subject: [PATCH 009/116] revert application lifecycle event support --- pkg/cmd/grafana-server/server.go | 7 ++++--- pkg/lifecycle/lifecycle.go | 22 -------------------- pkg/lifecycle/lifecycle_test.go | 35 -------------------------------- pkg/login/auth.go | 9 +++----- pkg/social/social.go | 9 +------- 5 files changed, 8 insertions(+), 74 deletions(-) delete mode 100644 pkg/lifecycle/lifecycle.go delete mode 100644 pkg/lifecycle/lifecycle_test.go diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index d4223df19ed..2c67a06a843 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -15,9 +15,10 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/lifecycle" + "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/social" "golang.org/x/sync/errgroup" @@ -69,7 +70,8 @@ func (g *GrafanaServerImpl) Run() error { g.loadConfiguration() g.writePIDFile() - lifecycle.Notify(lifecycle.ApplicationStarting) + login.Init() + social.NewOAuthService() serviceGraph := inject.Graph{} serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) @@ -142,7 +144,6 @@ func (g *GrafanaServerImpl) Run() error { } sendSystemdNotification("READY=1") - lifecycle.Notify(lifecycle.ApplicationStarted) return g.childRoutines.Wait() } diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go deleted file mode 100644 index eea733a7da8..00000000000 --- a/pkg/lifecycle/lifecycle.go +++ /dev/null @@ -1,22 +0,0 @@ -package lifecycle - -type Event int - -const ( - ApplicationStarting Event = iota - ApplicationStarted -) - -type EventHandlerFunc func() - -var listeners = map[int][]EventHandlerFunc{} - -func AddListener(evt Event, fn EventHandlerFunc) { - listeners[int(evt)] = append(listeners[int(evt)], fn) -} - -func Notify(evt Event) { - for _, handler := range listeners[int(evt)] { - handler() - } -} diff --git a/pkg/lifecycle/lifecycle_test.go b/pkg/lifecycle/lifecycle_test.go deleted file mode 100644 index e946cc2f4a8..00000000000 --- a/pkg/lifecycle/lifecycle_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package lifecycle - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestLifecycle(t *testing.T) { - Convey("TestLifecycle", t, func() { - Convey("Given listeners", func() { - applicationStartingCounter := 0 - AddListener(ApplicationStarting, func() { - applicationStartingCounter++ - }) - - applicationStartedCounter := 0 - AddListener(ApplicationStarted, func() { - applicationStartedCounter++ - }) - - Convey("When notify application starting should call listener", func() { - Notify(ApplicationStarting) - So(applicationStartingCounter, ShouldEqual, 1) - So(applicationStartedCounter, ShouldEqual, 0) - }) - - Convey("When notify application started should call listener", func() { - Notify(ApplicationStarted) - So(applicationStartingCounter, ShouldEqual, 0) - So(applicationStartedCounter, ShouldEqual, 1) - }) - }) - }) -} diff --git a/pkg/login/auth.go b/pkg/login/auth.go index b195b8dd2e4..b766d963328 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/lifecycle" m "github.com/grafana/grafana/pkg/models" ) @@ -20,11 +19,9 @@ var ( ErrGettingUserQuota = errors.New("Error getting user quota") ) -func init() { - lifecycle.AddListener(lifecycle.ApplicationStarting, func() { - bus.AddHandler("auth", AuthenticateUser) - loadLdapConfig() - }) +func Init() { + bus.AddHandler("auth", AuthenticateUser) + loadLdapConfig() } func AuthenticateUser(query *m.LoginUserQuery) error { diff --git a/pkg/social/social.go b/pkg/social/social.go index 054420fd994..8918507f3b9 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -8,18 +8,11 @@ import ( "golang.org/x/oauth2" - "github.com/grafana/grafana/pkg/lifecycle" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) -func init() { - lifecycle.AddListener(lifecycle.ApplicationStarting, func() { - initOAuthService() - }) -} - type BasicUserInfo struct { Id string Name string @@ -63,7 +56,7 @@ var ( allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", grafanaCom} ) -func initOAuthService() { +func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) From 6f9c0241afce94323f65e050a01d53357c4a1419 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Oct 2018 12:32:14 +0100 Subject: [PATCH 010/116] register datasource cache service with proper name --- pkg/services/datasources/cache.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/datasources/cache.go b/pkg/services/datasources/cache.go index c984b4f8743..0cd2bae63b5 100644 --- a/pkg/services/datasources/cache.go +++ b/pkg/services/datasources/cache.go @@ -20,7 +20,11 @@ type CacheServiceImpl struct { } func init() { - registry.RegisterService(&CacheServiceImpl{}) + registry.Register(®istry.Descriptor{ + Name: "DatasourceCacheService", + Instance: &CacheServiceImpl{}, + InitPriority: registry.Low, + }) } func (dc *CacheServiceImpl) Init() error { From d0c00388e6193cd438257935dba25b88eb56216a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Oct 2018 13:37:30 +0100 Subject: [PATCH 011/116] add functionality to override service in registry --- pkg/registry/registry.go | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 87fca27f6c1..487a6db7927 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -29,11 +29,42 @@ func Register(descriptor *Descriptor) { } func GetServices() []*Descriptor { - sort.Slice(services, func(i, j int) bool { - return services[i].InitPriority > services[j].InitPriority + slice := getServicesWithOverrides() + + sort.Slice(slice, func(i, j int) bool { + return slice[i].InitPriority > slice[j].InitPriority }) - return services + return slice +} + +type OverrideServiceFunc func(descriptor Descriptor) (*Descriptor, bool) + +var overrides []OverrideServiceFunc + +func RegisterOverride(fn OverrideServiceFunc) { + overrides = append(overrides, fn) +} + +func getServicesWithOverrides() []*Descriptor { + slice := []*Descriptor{} + for _, s := range services { + var descriptor *Descriptor + for _, fn := range overrides { + if newDescriptor, override := fn(*s); override { + descriptor = newDescriptor + break + } + } + + if descriptor != nil { + slice = append(slice, descriptor) + } else { + slice = append(slice, s) + } + } + + return slice } // Service interface is the lowest common shape that services From a8e2840f15e9957b28cdde280530376f4552fa69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 30 Oct 2018 15:25:10 +0100 Subject: [PATCH 012/116] minor progress --- docs/sources/enterprise/index.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index a241d84b50e..3583064f9c3 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -3,9 +3,30 @@ title = "Grafana Enterprise" description = "Grafana Enterprise overview" type = "docs" [menu.docs] -name = "Enterprise" +name = "Grafana Enterprise" identifier = "enterprise" -weight = 4 +weight = 5 +++ -### Grafana Enterprise +# Grafana Enterprise + +Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source +version. + +## Enterprise features + +Grafana Enterprise includes all of the features found in the open source version. Below we list the additional features +that can only be found in the Enterprise edition. + +### Enhanced LDAP + +With Grafana Enterprise you can setup syncing between LDAP Groups and Teams. [Learn More](link). + +### Data source permissions + +Assign and restrict query permissions on Data Sources to specific teams or users. [Learn More](link). + +## Try Grafana Enterprise + +## Licence file mangement + From 621525d10fa636e51c932c364c6f08fcc96e5a32 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Oct 2018 18:43:54 +0100 Subject: [PATCH 013/116] restructure administration/permissions page into a section with sub pages --- docs/sources/administration/permissions.md | 116 ------------------ docs/sources/enterprise/index.md | 4 +- .../dashboard_folder_permissions.md | 67 ++++++++++ .../permissions/datasource_permissions.md | 71 +++++++++++ docs/sources/permissions/index.md | 12 ++ .../sources/permissions/organization_roles.md | 38 ++++++ docs/sources/permissions/overview.md | 42 +++++++ 7 files changed, 232 insertions(+), 118 deletions(-) delete mode 100644 docs/sources/administration/permissions.md create mode 100644 docs/sources/permissions/dashboard_folder_permissions.md create mode 100644 docs/sources/permissions/datasource_permissions.md create mode 100644 docs/sources/permissions/index.md create mode 100644 docs/sources/permissions/organization_roles.md create mode 100644 docs/sources/permissions/overview.md diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md deleted file mode 100644 index 0d374f03647..00000000000 --- a/docs/sources/administration/permissions.md +++ /dev/null @@ -1,116 +0,0 @@ -+++ -title = "Permissions" -description = "Grafana user permissions" -keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] -type = "docs" -aliases = ["/reference/admin"] -[menu.docs] -name = "Permissions" -parent = "admin" -weight = 3 -+++ - -# Permissions - -Grafana users have permissions that are determined by their: - -- **Organization Role** (Admin, Editor, Viewer) -- Via **Team** memberships where the **Team** has been assigned specific permissions. -- Via permissions assigned directly to user (on folders or dashboards) -- The Grafana Admin (i.e. Super Admin) user flag. - -## Organization Roles - -Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do -in that organization. - -### Admin Role - -Can do everything scoped to the organization. For example: - -- Add & Edit data sources. -- Add & Edit organization users & teams. -- Configure App plugins & set org settings. - -### Editor Role - -- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards. -- **Cannot** create or edit data sources nor invite new users. - -### Viewer Role - -- View any dashboard. This can be disabled on specific folders and dashboards. -- **Cannot** create or edit dashboards nor data sources. - -This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users -with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards). -Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards. - -## Grafana Admin - -This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated. - -### Dashboard & Folder Permissions - -{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} - -For dashboards and dashboard folders there is a **Permissions** page that make it possible to -remove the default role based permissions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. - -You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. - -Permission levels: - -- **Admin**: Can edit & create dashboards and edit permissions. -- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. -- **View**: Can only view existing dashboards/folders. - -#### Restricting Access - -The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the Access Control List (ACL). - -- You cannot override permissions for users with the **Org Admin Role**. Admins always have access to everything. -- A more specific permission with a lower permission level will not have any effect if a more general rule exists with higher permission level. You need to remove or lower the permission level of the more general rule. - -#### How Grafana Resolves Multiple Permissions - Examples - -##### Example 1 (`user1` has the Editor Role) - -Permissions for a dashboard: - -- `Everyone with Editor Role Can Edit` -- `user1 Can View` - -Result: `user1` has Edit permission as the highest permission always wins. - -##### Example 2 (`user1` has the Viewer Role and is a member of `team1`) - -Permissions for a dashboard: - -- `Everyone with Viewer Role Can View` -- `user1 Can Edit` -- `team1 Can Admin` - -Result: `user1` has Admin permission as the highest permission always wins. - -##### Example 3 - -Permissions for a dashboard: - -- `user1 Can Admin (inherited from parent folder)` -- `user1 Can Edit` - -Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins. - -- **View**: Can only view existing dashboards/folders. -- You cannot override permissions for users with **Org Admin Role** -- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. - -### Data source permissions - -Permissions on dashboards and folders **do not** include permissions on data sources. A user with `Viewer` role -can still issue any possible query to a data source, not just those queries that exist on dashboards he/she has access to. -We hope to add permissions on data sources in a future release. Until then **do not** view dashboard permissions as a secure -way to restrict user data access. Dashboard permissions only limits what dashboards & folders a user can view & edit not which -data sources a user can access nor what queries a user can issue. - diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 3583064f9c3..378de9d6371 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -22,9 +22,9 @@ that can only be found in the Enterprise edition. With Grafana Enterprise you can setup syncing between LDAP Groups and Teams. [Learn More](link). -### Data source permissions +### Datasource Permissions -Assign and restrict query permissions on Data Sources to specific teams or users. [Learn More](link). +Datasource permissions allows you to restrict access for users to query a datasource. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). ## Try Grafana Enterprise diff --git a/docs/sources/permissions/dashboard_folder_permissions.md b/docs/sources/permissions/dashboard_folder_permissions.md new file mode 100644 index 00000000000..fb82f00d712 --- /dev/null +++ b/docs/sources/permissions/dashboard_folder_permissions.md @@ -0,0 +1,67 @@ ++++ +title = "Dashboard & Folder Permissions" +description = "Grafana Dashboard & Folder Permissions Guide " +keywords = ["grafana", "configuration", "documentation", "dashboard", "folder", "permissions", "teams"] +type = "docs" +[menu.docs] +name = "Dashboard & Folder Permissions" +identifier = "dashboard-folder-permissions" +parent = "permissions" +weight = 3 ++++ + +# Dashboard & Folder Permissions + +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} + +For dashboards and dashboard folders there is a **Permissions** page that make it possible to +remove the default role based permissions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. + +You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. + +Permission levels: + +- **Admin**: Can edit & create dashboards and edit permissions. +- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. +- **View**: Can only view existing dashboards/folders. + +## Restricting Access + +The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the Access Control List (ACL). + +- You cannot override permissions for users with the **Org Admin Role**. Admins always have access to everything. +- A more specific permission with a lower permission level will not have any effect if a more general rule exists with higher permission level. You need to remove or lower the permission level of the more general rule. + +### How Grafana Resolves Multiple Permissions - Examples + +#### Example 1 (`user1` has the Editor Role) + +Permissions for a dashboard: + +- `Everyone with Editor Role Can Edit` +- `user1 Can View` + +Result: `user1` has Edit permission as the highest permission always wins. + +#### Example 2 (`user1` has the Viewer Role and is a member of `team1`) + +Permissions for a dashboard: + +- `Everyone with Viewer Role Can View` +- `user1 Can Edit` +- `team1 Can Admin` + +Result: `user1` has Admin permission as the highest permission always wins. + +#### Example 3 + +Permissions for a dashboard: + +- `user1 Can Admin (inherited from parent folder)` +- `user1 Can Edit` + +Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins. + +- **View**: Can only view existing dashboards/folders. +- You cannot override permissions for users with **Org Admin Role** +- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md new file mode 100644 index 00000000000..fd5405fd684 --- /dev/null +++ b/docs/sources/permissions/datasource_permissions.md @@ -0,0 +1,71 @@ ++++ +title = "Datasource Permissions" +description = "Grafana Datasource Permissions Guide " +keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams"] +type = "docs" +[menu.docs] +name = "Datasource Permissions" +identifier = "datasource-permissions" +parent = "permissions" +weight = 4 ++++ + +# Datasource Permissions + +> Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +Datasource permissions allows you to restrict access for users to query a datasource. For each datasource there is +a permission page that makes it possible to enable permissions and add restrict query permissions to specific +**Users** and **Teams**. + +## Restricting Access - Enable Permissions + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_enable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_enable.gif" >}} + +By default, permissions are disabled for datasources and a datasource in an organization can be queried by any user in +that organization. For example a user with `Viewer` role can still issue any possible query to a datasource, not just +those queries that exist on dashboards he/she has access to. + +When permissions are enabled for a datasource in an organization you will restrict admin and query access for that +datasource to [admin users](/permissions/organization_roles/#admin-role) in that organization. + +**To enable permissions for a datasource:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to enable permissions for. +3. Select the Permissions tab and click on the `Enable` button. + +
+ +## Allow users and teams to query a datasource + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_add_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_add.gif" >}} + +After you have [enabled permissions](#restricting-access-enable-permissions) for a datasource you can assign query +permissions to users and teams which will allow access to query the datasource. + +**Assign query permission to users and teams:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to assign query permissions for. +3. Select the Permissions tab. +4. click on the `Add Permission` button. +5. Select Team/User and find the team/user you want to allow query access and click on the `Save` button. + +
+ +## Restore Default Access - Disable Permissions + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_disable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_disable.gif" >}} + +If you have enabled permissions for a datasource and want to revoke datasource permissions to the default, i.e. +datasource can be queried by any user in that organization, you can disable permissions with a click of a button. +Note that all existing permissions created for datasource will be deleted. + +**To disable permissions for a datasource:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to disable permissions for. +3. Select the Permissions tab and click on the `Disable Permissions` button. + +
diff --git a/docs/sources/permissions/index.md b/docs/sources/permissions/index.md new file mode 100644 index 00000000000..42514f76baf --- /dev/null +++ b/docs/sources/permissions/index.md @@ -0,0 +1,12 @@ ++++ +title = "Permissions" +description = "Permissions" +type = "docs" +[menu.docs] +name = "Permissions" +identifier = "permissions" +parent = "admin" +weight = 3 ++++ + + diff --git a/docs/sources/permissions/organization_roles.md b/docs/sources/permissions/organization_roles.md new file mode 100644 index 00000000000..626d79fad87 --- /dev/null +++ b/docs/sources/permissions/organization_roles.md @@ -0,0 +1,38 @@ ++++ +title = "Organization Roles" +description = "Grafana Organization Roles Guide " +keywords = ["grafana", "configuration", "documentation", "organization", "roles", "permissions"] +type = "docs" +[menu.docs] +name = "Organization Roles" +identifier = "organization-roles" +parent = "permissions" +weight = 2 ++++ + +# Organization Roles + +Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do +in that organization. + +## Admin Role + +Can do everything scoped to the organization. For example: + +- Add & Edit data sources. +- Add & Edit organization users & teams. +- Configure App plugins & set org settings. + +## Editor Role + +- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit data sources nor invite new users. + +## Viewer Role + +- View any dashboard. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit dashboards nor data sources. + +This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users +with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards). +Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards. diff --git a/docs/sources/permissions/overview.md b/docs/sources/permissions/overview.md new file mode 100644 index 00000000000..cd3fc5417b6 --- /dev/null +++ b/docs/sources/permissions/overview.md @@ -0,0 +1,42 @@ ++++ +title = "Overview" +description = "Overview for permissions" +keywords = ["grafana", "configuration", "documentation", "admin", "users", "datasources", "permissions"] +type = "docs" +aliases = ["/reference/admin", "/administration/permissions/"] +[menu.docs] +name = "Overview" +identifier = "overview-permissions" +parent = "permissions" +weight = 1 ++++ + +# Permissions Overview + +Grafana users have permissions that are determined by their: + +- **Organization Role** (Admin, Editor, Viewer) +- Via **Team** memberships where the **Team** has been assigned specific permissions. +- Via permissions assigned directly to user (on folders, dashboards, datasources) +- The Grafana Admin (i.e. Super Admin) user flag. + +## Grafana Admin + +This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated. + +## Organization Roles + +Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do +in that organization. Learn more about [Organization Roles]({{< relref "permissions/organization_roles.md" >}}). + + +## Dashboard & Folder Permissions + +Dashboard and folder permissions allows you to remove the default role based permissions for Editors and Viewers and assign permissions to specific **Users** and **Teams**. Learn more about [Dashboard & Folder Permissions]({{< relref "permissions/dashboard_folder_permissions.md" >}}). + +## Datasource Permissions + +Per default, a datasource in an organization can be queried by any user in that organization. For example a user with `Viewer` role can still +issue any possible query to a data source, not just those queries that exist on dashboards he/she has access to. + +Datasource permissions allows you to change the default permissions for datasources and restrict query permissions to specific **Users** and **Teams**. Read more about [Datasource Permissions]({{< relref "permissions/datasource_permissions.md" >}}). From fc6d7c9b6b041ffddb07df3cb6b02dc2fd299a0a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Oct 2018 19:02:12 +0100 Subject: [PATCH 014/116] datasource permission http api --- .../http_api/datasource_permissions.md | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/sources/http_api/datasource_permissions.md diff --git a/docs/sources/http_api/datasource_permissions.md b/docs/sources/http_api/datasource_permissions.md new file mode 100644 index 00000000000..aa4d498ef85 --- /dev/null +++ b/docs/sources/http_api/datasource_permissions.md @@ -0,0 +1,249 @@ ++++ +title = "Datasource Permissions HTTP API " +description = "Grafana Datasource Permissions HTTP API" +keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl"] +aliases = ["/http_api/datasourcepermissions/"] +type = "docs" +[menu.docs] +name = "Datasource Permissions" +parent = "http_api" ++++ + +# Datasource Permissions API + +> Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +This API can be used to enable, disable, list, add and remove permissions for a datasource. + +Permissions can be set for a user or a team. Permissions cannot be set for Admins - they always have access to everything. + +The permission levels for the permission field: + +- 1 = Query + +## Enable permissions for a datasource + +`POST /api/datasources/:id/enable-permissions` + +Enables permissions for the datasource with the given `id`. No one except Org Admins will be able to query the datasource until a permission have been added which permits certain users or teams to query the datasource. + +**Example request**: + +```http +POST /api/datasources/1/enable-permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permissions enabled"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permissions cannot be enabled, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Disable permissions for a datasource + +`POST /api/datasources/:id/disable-permissions` + +Disables permissions for the datasource with the given `id`. All existing permissions will be removed and anyone will be able to query the datasource. + +**Example request**: + +```http +POST /api/datasources/1/disable-permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permissions disabled"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permissions cannot be disabled, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Get permissions for a datasource + +`GET /api/datasources/:id/permissions` + +Gets all existing permissions for the datasource with the given `id`. + +**Example request**: + +```http +GET /api/datasources/1/permissions HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 551 + +{ + "datasourceId": 1, + "enabled": true, + "permissions": + [ + { + "id": 1, + "datasourceId": 1, + "userId": 1, + "userLogin": "user", + "userEmail": "user@test.com", + "userAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "permission": 1, + "permissionName": "Query", + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + }, + { + "id": 2, + "datasourceId": 1, + "teamId": 1, + "team": "A Team", + "teamAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "permission": 1, + "permissionName": "Query", + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + } + ] +} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Add permission for a datasource + +`POST /api/datasources/:id/permissions` + +Adds a user permission for the datasource with the given `id`. + +**Example request**: + +```http +POST /api/datasources/1/permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "userId": 1, + "permission": 1 +} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permission added"} +``` + +Adds a team permission for the datasource with the given `id`. + +**Example request**: + +```http +POST /api/datasources/1/permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "teamId": 1, + "permission": 1 +} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permission added"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permission cannot be added, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Remove permission for a datasource + +`DELETE /api/datasources/:id/permissions/:permissionId` + +Removes the permission with the given `permissionId` for the datasource with the given `id`. + +**Example request**: + +```http +DELETE /api/datasources/1/permissions/2 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permission removed"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found or permission not found From 93453c2d94856cec8861e1b627ba0c47c65061d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 31 Oct 2018 06:47:14 -0700 Subject: [PATCH 015/116] added caching of signed in user DB calls --- pkg/services/sqlstore/sqlstore.go | 10 +++++++--- pkg/services/sqlstore/user.go | 15 +++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index f904b44c3c8..9f2d83d3284 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -16,6 +16,7 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" @@ -47,8 +48,9 @@ func init() { } type SqlStore struct { - Cfg *setting.Cfg `inject:""` - Bus bus.Bus `inject:""` + Cfg *setting.Cfg `inject:""` + Bus bus.Bus `inject:""` + CacheService *cache.CacheService `inject:""` dbCfg DatabaseConfig engine *xorm.Engine @@ -148,9 +150,11 @@ func (ss *SqlStore) Init() error { // Init repo instances annotations.SetRepository(&SqlAnnotationRepo{}) - ss.Bus.SetTransactionManager(ss) + // Register handlers + ss.addUserQueryAndCommandHandlers() + // ensure admin user if ss.skipEnsureAdmin { return nil diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 3744a04d88a..8e5d61579b9 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -15,8 +15,9 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func init() { - //bus.AddHandler("sql", CreateUser) +func (ss *SqlStore) addUserQueryAndCommandHandlers() { + ss.Bus.AddHandler(ss.GetSignedInUser) + bus.AddHandler("sql", GetUserById) bus.AddHandler("sql", UpdateUser) bus.AddHandler("sql", ChangeUserPassword) @@ -25,7 +26,6 @@ func init() { bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", UpdateUserLastSeenAt) bus.AddHandler("sql", GetUserProfile) - bus.AddHandler("sql", GetSignedInUser) bus.AddHandler("sql", SearchUsers) bus.AddHandler("sql", GetUserOrgList) bus.AddHandler("sql", DeleteUser) @@ -345,12 +345,18 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error { return err } -func GetSignedInUser(query *m.GetSignedInUserQuery) error { +func (ss *SqlStore) GetSignedInUser(query *m.GetSignedInUserQuery) error { orgId := "u.org_id" if query.OrgId > 0 { orgId = strconv.FormatInt(query.OrgId, 10) } + cacheKey := fmt.Sprintf("signed-in-user-%d-%s", query.UserId, query.OrgId) + if cached, found := ss.CacheService.Get(cacheKey); found { + query.Result = cached.(*m.SignedInUser) + return nil + } + var rawSql = `SELECT u.id as user_id, u.is_admin as is_grafana_admin, @@ -401,6 +407,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { } query.Result = &user + ss.CacheService.Set(cacheKey, &user, time.Second*5) return err } From 5495072c83ef872bbc3b797efb02d05811547b24 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Oct 2018 17:17:19 +0100 Subject: [PATCH 016/116] docs: fix datasource permissions keywords --- docs/sources/http_api/datasource_permissions.md | 2 +- docs/sources/permissions/datasource_permissions.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/http_api/datasource_permissions.md b/docs/sources/http_api/datasource_permissions.md index aa4d498ef85..bc193113b43 100644 --- a/docs/sources/http_api/datasource_permissions.md +++ b/docs/sources/http_api/datasource_permissions.md @@ -1,7 +1,7 @@ +++ title = "Datasource Permissions HTTP API " description = "Grafana Datasource Permissions HTTP API" -keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl"] +keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl", "enterprise"] aliases = ["/http_api/datasourcepermissions/"] type = "docs" [menu.docs] diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md index fd5405fd684..f1cbd31b85f 100644 --- a/docs/sources/permissions/datasource_permissions.md +++ b/docs/sources/permissions/datasource_permissions.md @@ -1,7 +1,7 @@ +++ title = "Datasource Permissions" description = "Grafana Datasource Permissions Guide " -keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams"] +keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"] type = "docs" [menu.docs] name = "Datasource Permissions" From 280c8631f924c570fe933c0f7293336732989e28 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Oct 2018 18:01:30 +0100 Subject: [PATCH 017/116] docs: enhanced ldap --- docs/sources/auth/enhanced_ldap.md | 43 +++++++ docs/sources/enterprise/index.md | 7 +- docs/sources/http_api/external_group_sync.md | 111 +++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 docs/sources/auth/enhanced_ldap.md create mode 100644 docs/sources/http_api/external_group_sync.md diff --git a/docs/sources/auth/enhanced_ldap.md b/docs/sources/auth/enhanced_ldap.md new file mode 100644 index 00000000000..8eec57b1429 --- /dev/null +++ b/docs/sources/auth/enhanced_ldap.md @@ -0,0 +1,43 @@ ++++ +title = "Enhanced LDAP Integration" +description = "Grafana Enhanced LDAP Integration Guide " +keywords = ["grafana", "configuration", "documentation", "ldap", "active directory", "enterprise"] +type = "docs" +[menu.docs] +name = "Enhanced LDAP" +identifier = "enhanced-ldap" +parent = "authentication" +weight = 3 ++++ + +# Enhanced LDAP Integration + +> Enhanced LDAP Integration is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +The enhanced LDAP integration adds additional functionality on top of the [existing LDAP integration]({{< relref "auth/ldap.md" >}}). + +## LDAP Group Synchronization for Teams + +{{< docs-imagebox img="/img/docs/enterprise/team_members_ldap.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}} + +With the enhanced LDAP integration it's possible to setup synchronization between LDAP groups and teams. This enables LDAP users which are members +of certain LDAP groups to automatically be added/removed as members to certain teams in Grafana. Currently the synchronization will only happen every +time a user logs in, but an active background synchronization is currently being developed. + +Grafana keeps track of all synchronized users in teams and you can see which users have been synchronized from LDAP in the team members list, see `LDAP` label in screenshot. +This mechanism allows Grafana to remove an existing synchronized user from a team when its LDAP group membership changes. This mechanism also enables you to manually add +a user as member of a team and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships. + +
+ +### Enable LDAP group synchronization for a team + +{{< docs-imagebox img="/img/docs/enterprise/team_add_external_group.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}} + +1. Navigate to Configuration / Teams. +2. Select a team. +3. Select the External group sync tab and click on the `Add group` button. +4. Insert LDAP distinguished name (DN) of LDAP group you want to synchronize with the team. +5. Click on `Add group` button to save. + +
diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 378de9d6371..97a06f1ab47 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -1,6 +1,7 @@ +++ title = "Grafana Enterprise" description = "Grafana Enterprise overview" +keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise"] type = "docs" [menu.docs] name = "Grafana Enterprise" @@ -18,9 +19,9 @@ version. Grafana Enterprise includes all of the features found in the open source version. Below we list the additional features that can only be found in the Enterprise edition. -### Enhanced LDAP +### Enhanced LDAP Integration -With Grafana Enterprise you can setup syncing between LDAP Groups and Teams. [Learn More](link). +With Grafana Enterprise you can setup synchronization between LDAP Groups and Teams. [Learn More]({{< relref "auth/enhanced_ldap.md" >}}). ### Datasource Permissions @@ -28,5 +29,5 @@ Datasource permissions allows you to restrict access for users to query a dataso ## Try Grafana Enterprise -## Licence file mangement +## Licence file management diff --git a/docs/sources/http_api/external_group_sync.md b/docs/sources/http_api/external_group_sync.md new file mode 100644 index 00000000000..2ce06c2c94e --- /dev/null +++ b/docs/sources/http_api/external_group_sync.md @@ -0,0 +1,111 @@ ++++ +title = "External Group Sync HTTP API " +description = "Grafana External Group Sync HTTP API" +keywords = ["grafana", "http", "documentation", "api", "team", "teams", "group", "member", "enterprise"] +aliases = ["/http_api/external_group_sync/"] +type = "docs" +[menu.docs] +name = "External Group Sync" +parent = "http_api" ++++ + +# External Group Synchronization API + +> External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +## Get External Groups + +`GET /api/teams/:teamId/groups` + +**Example Request**: + +```http +GET /api/teams/1/groups HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "orgId": 1, + "teamId": 1, + "groupId": "cn=editors,ou=groups,dc=grafana,dc=org" + } +] +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied + +## Add External Group + +`POST /api/teams/:teamId/groups` + +**Example Request**: + +```http +POST /api/teams/1/members HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= + +{ + "groupId": "cn=editors,ou=groups,dc=grafana,dc=org" +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Group added to Team"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Group is already added to this team +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Team not found + +## Remove External Group + +`DELETE /api/teams/:teamId/groups/:groupId` + +**Example Request**: + +```http +DELETE /api/teams/1/groups/cn=editors,ou=groups,dc=grafana,dc=org HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Team Group removed"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Team not found/Group not found From edd575b552374eeb81e9007aedf0ce1123e20420 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 1 Nov 2018 09:36:09 +0100 Subject: [PATCH 018/116] Explore: fix metric selector for additional rows - race condition in language provider leads to only one row getting selector options - fixed by always returning the start task promise --- .../logging/components/LoggingQueryField.tsx | 8 ++++---- .../plugins/datasource/logging/language_provider.ts | 8 +++----- .../prometheus/components/PromQueryField.tsx | 6 +++--- .../plugins/datasource/prometheus/language_provider.ts | 10 ++++------ public/app/types/explore.ts | 3 ++- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx index 82ebc444c62..ce79d38f9a8 100644 --- a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx @@ -95,9 +95,9 @@ class LoggingQueryField extends React.PureComponent { - remaining.map(task => task.then(this.onReceiveMetrics).catch(() => {})); + remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {})); }) - .then(() => this.onReceiveMetrics()); + .then(() => this.onUpdateLanguage()); } } @@ -119,7 +119,7 @@ class LoggingQueryField extends React.PureComponent {}); }; @@ -147,7 +147,7 @@ class LoggingQueryField extends React.PureComponent { + onUpdateLanguage = () => { Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax(); const { logLabelOptions } = this.languageProvider; this.setState({ diff --git a/public/app/plugins/datasource/logging/language_provider.ts b/public/app/plugins/datasource/logging/language_provider.ts index 32f4f8a0cd7..0896168ca56 100644 --- a/public/app/plugins/datasource/logging/language_provider.ts +++ b/public/app/plugins/datasource/logging/language_provider.ts @@ -47,7 +47,6 @@ export default class LoggingLanguageProvider extends LanguageProvider { this.datasource = datasource; this.labelKeys = {}; this.labelValues = {}; - this.started = false; Object.assign(this, initialValues); } @@ -63,11 +62,10 @@ export default class LoggingLanguageProvider extends LanguageProvider { }; start = () => { - if (!this.started) { - this.started = true; - return this.fetchLogLabels(); + if (!this.startTask) { + this.startTask = this.fetchLogLabels(); } - return Promise.resolve([]); + return this.startTask; }; // Keep this DOM-free for testing diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 649b17ad8cf..a7787096d85 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -134,9 +134,9 @@ class PromQueryField extends React.PureComponent { - remaining.map(task => task.then(this.onReceiveMetrics).catch(() => {})); + remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {})); }) - .then(() => this.onReceiveMetrics()); + .then(() => this.onUpdateLanguage()); } } @@ -176,7 +176,7 @@ class PromQueryField extends React.PureComponent { + onUpdateLanguage = () => { const { histogramMetrics, metrics } = this.languageProvider; if (!metrics) { return; diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 5f97ebfa7b5..23c25885041 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -46,7 +46,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...] labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] metrics?: string[]; - started: boolean; + startTask: Promise; constructor(datasource: any, initialValues?: any) { super(); @@ -56,7 +56,6 @@ export default class PromQlLanguageProvider extends LanguageProvider { this.labelKeys = {}; this.labelValues = {}; this.metrics = []; - this.started = false; Object.assign(this, initialValues); } @@ -72,11 +71,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; start = () => { - if (!this.started) { - this.started = true; - return this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]); + if (!this.startTask) { + this.startTask = this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]); } - return Promise.resolve([]); + return this.startTask; }; // Keep this DOM-free for testing diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index a96d6c084fb..5a9db7e9b53 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -86,10 +86,11 @@ export abstract class LanguageProvider { datasource: any; request: (url) => Promise; /** - * Returns a promise that resolves with a task list when main syntax is loaded. + * Returns startTask that resolves with a task list when main syntax is loaded. * Task list consists of secondary promises that load more detailed language features. */ start: () => Promise; + startTask?: Promise; } export interface TypeaheadInput { From a1b4ebc11516046b94b6b1bfed3a9fe4f834ba2b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Nov 2018 11:00:32 +0100 Subject: [PATCH 019/116] make permission sub items in sidemenu cleaner --- docs/sources/permissions/dashboard_folder_permissions.md | 2 +- docs/sources/permissions/datasource_permissions.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/permissions/dashboard_folder_permissions.md b/docs/sources/permissions/dashboard_folder_permissions.md index fb82f00d712..aed0f91ee7c 100644 --- a/docs/sources/permissions/dashboard_folder_permissions.md +++ b/docs/sources/permissions/dashboard_folder_permissions.md @@ -4,7 +4,7 @@ description = "Grafana Dashboard & Folder Permissions Guide " keywords = ["grafana", "configuration", "documentation", "dashboard", "folder", "permissions", "teams"] type = "docs" [menu.docs] -name = "Dashboard & Folder Permissions" +name = "Dashboard & Folder" identifier = "dashboard-folder-permissions" parent = "permissions" weight = 3 diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md index f1cbd31b85f..f94fc47c4d2 100644 --- a/docs/sources/permissions/datasource_permissions.md +++ b/docs/sources/permissions/datasource_permissions.md @@ -4,7 +4,7 @@ description = "Grafana Datasource Permissions Guide " keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"] type = "docs" [menu.docs] -name = "Datasource Permissions" +name = "Datasource" identifier = "datasource-permissions" parent = "permissions" weight = 4 From 5a27df2dc96c76766a178627048a03d0a3b08365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Nov 2018 12:17:04 +0100 Subject: [PATCH 020/116] updated enterprise page --- docs/sources/enterprise/index.md | 39 +++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 97a06f1ab47..d0e7798d3d8 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -14,7 +14,8 @@ weight = 5 Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source version. -## Enterprise features +Building on everything you already know and love about Grafana, Grafana Enterprise layers on [premium data sources](https://grafana.com/plugins?premium=1), + advanced authentication options, **Data Source** permissions and 24x7x365 support and training from the core Grafana team. Grafana Enterprise includes all of the features found in the open source version. Below we list the additional features that can only be found in the Enterprise edition. @@ -25,9 +26,41 @@ With Grafana Enterprise you can setup synchronization between LDAP Groups and Te ### Datasource Permissions -Datasource permissions allows you to restrict access for users to query a datasource. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). +Datasource permissions allows you to restrict query access to only specific Teams and Users. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). + +### Premium Plugins + +With a Grafana Enterprise licence you will get access to these premium plugins. + +* [Splunk](https://grafana.com/plugins/grafana-splunk-datasource) +* [AppDynamics](https://grafana.com/plugins/dlopes7-appdynamics-datasource) +* [DataDog](https://grafana.com/plugins/grafana-datadog-datasource) +* [Dynatrace](https://grafana.com/plugins/grafana-dynatrace-datasource) +* [New Relic](https://grafana.com/plugins/grafana-newrelic-datasource) ## Try Grafana Enterprise -## Licence file management +You can learn more about Grafana Enterprise [here](https://grafana.com/enterprise). To purchase or obtain a trial license contact +the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise). + +## License file management + +To download your Grafana Enterprise license login to you [Grafana.com](https://grafana.com) account and go to your **Org +Profile**. In the side menu there is a section for Grafana Enterprise licenses. At the bottom of the license +details page there is **Download Token** link that will download the *license.jwt* file containing your license. + +Place the *license.jwt* file in Grafana's data folder. This is usually located at `/var/lib/grafana/data` on linux systems. + +You can also configure a custom location for the license file via the ini setting: + +```bash +[enterprise] +license_path = /company/secrets/license.jwt +``` + +This setting can also be set via ENV variable. Which is useful if your running Grafana via docker and have a custom +volume where you have placed the license file. In this case set the ENV variable `GF_ENTERPRISE_LICENSE_PATH` to point +to the location of your license file. + + From 4c070bc781662064e4702c674f6f40d841f88ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Nov 2018 12:35:51 +0100 Subject: [PATCH 021/116] minor doc tweaks --- docs/sources/enterprise/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index d0e7798d3d8..6d5f77ab6c8 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -14,11 +14,11 @@ weight = 5 Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source version. -Building on everything you already know and love about Grafana, Grafana Enterprise layers on [premium data sources](https://grafana.com/plugins?premium=1), - advanced authentication options, **Data Source** permissions and 24x7x365 support and training from the core Grafana team. +Building on everything you already know and love about Grafana, Grafana Enterprise layers on premium data sources. +advanced authentication options, more permissions controls and 24x7x365 support and training from the core Grafana team. -Grafana Enterprise includes all of the features found in the open source version. Below we list the additional features -that can only be found in the Enterprise edition. +Grafana Enterprise includes all of the features found in the open source edition. Below we list the additional features +that can only be found in the Grafana Enterprise. ### Enhanced LDAP Integration From 583334df051ca736140663256c6bed2303c34f84 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 2 Nov 2018 08:25:36 +0100 Subject: [PATCH 022/116] Explore: Logging graph overview and view options - Logging gets a graph for log distribution (currently per stream, but I think I'll change that to per log-level) - added grid columns for timestamp and unique labels - show common labels of streams - View options to show/hide time columns, label columns - created `--small` modifier for Switch CSS classes - merging of streams is now a datasource responsibility --- public/app/core/components/Switch/Switch.tsx | 11 +- public/app/core/logs_model.ts | 34 +++-- public/app/features/explore/Explore.tsx | 16 ++- public/app/features/explore/Graph.tsx | 5 +- public/app/features/explore/Logs.tsx | 109 ++++++++++++++- .../plugins/datasource/logging/datasource.ts | 13 +- .../logging/result_transformer.test.ts | 56 +++++++- .../datasource/logging/result_transformer.ts | 132 +++++++++++++++++- public/sass/components/_gf-form.scss | 5 + public/sass/components/_switch.scss | 15 +- public/sass/pages/_explore.scss | 37 ++++- 11 files changed, 397 insertions(+), 36 deletions(-) diff --git a/public/app/core/components/Switch/Switch.tsx b/public/app/core/components/Switch/Switch.tsx index ba09267ebd2..a4bb73a291b 100644 --- a/public/app/core/components/Switch/Switch.tsx +++ b/public/app/core/components/Switch/Switch.tsx @@ -5,6 +5,7 @@ export interface Props { label: string; checked: boolean; labelClass?: string; + small?: boolean; switchClass?: string; onChange: (event) => any; } @@ -24,10 +25,14 @@ export class Switch extends PureComponent { }; render() { - const { labelClass, switchClass, label, checked } = this.props; + const { labelClass = '', switchClass = '', label, checked, small } = this.props; const labelId = `check-${this.state.id}`; - const labelClassName = `gf-form-label ${labelClass} pointer`; - const switchClassName = `gf-form-switch ${switchClass}`; + let labelClassName = `gf-form-label ${labelClass} pointer`; + let switchClassName = `gf-form-switch ${switchClass}`; + if (small) { + labelClassName += ' gf-form-label--small'; + switchClassName += ' gf-form-switch--small'; + } return (
diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index e6f317dbeb7..ca7899db7d8 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { TimeSeries } from 'app/core/core'; export enum LogLevel { crit = 'crit', @@ -19,25 +20,34 @@ export interface LogSearchMatch { export interface LogRow { key: string; entry: string; + labels: string; logLevel: LogLevel; timestamp: string; timeFromNow: string; + timeJs: number; timeLocal: string; searchWords?: string[]; } -export interface LogsModel { - rows: LogRow[]; +export interface LogsMetaItem { + label: string; + value: string; } -export function mergeStreams(streams: LogsModel[], limit?: number): LogsModel { - const combinedEntries = streams.reduce((acc, stream) => { - return [...acc, ...stream.rows]; - }, []); - const sortedEntries = _.chain(combinedEntries) - .sortBy('timestamp') - .reverse() - .slice(0, limit || combinedEntries.length) - .value(); - return { rows: sortedEntries }; +export interface LogsModel { + meta?: LogsMetaItem[]; + rows: LogRow[]; + series?: TimeSeries[]; +} + +export interface LogsStream { + labels: string; + entries: LogsStreamEntry[]; + parsedLabels: { [key: string]: string }; + graphSeries: TimeSeries; +} + +export interface LogsStreamEntry { + line: string; + timestamp: string; } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index af771bad5dd..6ec6c79ac5f 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -25,7 +25,6 @@ import ErrorBoundary from './ErrorBoundary'; import TimePicker from './TimePicker'; import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; import { DataSource } from 'app/types/datasources'; -import { mergeStreams } from 'app/core/logs_model'; const MAX_HISTORY_ITEMS = 100; @@ -770,9 +769,14 @@ export class Explore extends React.PureComponent { new TableModel(), ...queryTransactions.filter(qt => qt.resultType === 'Table' && qt.done && qt.result).map(qt => qt.result) ); - const logsResult = mergeStreams( - queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result) - ); + const logsResult = + datasource && datasource.mergeStreams + ? datasource.mergeStreams( + _.flatten( + queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result) + ) + ) + : undefined; const loading = queryTransactions.some(qt => !qt.done); const showStartPages = StartPage && queryTransactions.length === 0; const viewModeCount = [supportsGraph, supportsLogs, supportsTable].filter(m => m).length; @@ -903,7 +907,9 @@ export class Explore extends React.PureComponent { ) : null} - {supportsLogs && showingLogs ? : null} + {supportsLogs && showingLogs ? ( + + ) : null} )} diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index bbb055067a2..6c22ca67509 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -79,6 +79,7 @@ interface GraphProps { range: RawTimeRange; split?: boolean; size?: { width: number; height: number }; + userOptions?: any; } interface GraphState { @@ -122,7 +123,7 @@ export class Graph extends PureComponent { }; draw() { - const { range, size } = this.props; + const { range, size, userOptions = {} } = this.props; const data = this.getGraphData(); const $el = $(`#${this.props.id}`); @@ -153,12 +154,14 @@ export class Graph extends PureComponent { max: max, label: 'Datetime', ticks: ticks, + timezone: 'browser', timeformat: time_format(ticks, min, max), }, }; const options = { ...FLOT_OPTIONS, ...dynamicOptions, + ...userOptions, }; $.plot($el, series, options); } diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 278c5ee016d..5630d1de8f6 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -1,29 +1,130 @@ import React, { Fragment, PureComponent } from 'react'; import Highlighter from 'react-highlight-words'; +import { RawTimeRange } from 'app/types/series'; import { LogsModel } from 'app/core/logs_model'; import { findHighlightChunksInText } from 'app/core/utils/text'; +import { Switch } from 'app/core/components/Switch/Switch'; + +import Graph from './Graph'; + +const graphOptions = { + series: { + bars: { + show: true, + }, + }, + yaxis: { + tickDecimals: 0, + }, +}; interface LogsProps { className?: string; data: LogsModel; loading: boolean; + position: string; + range?: RawTimeRange; } -export default class Logs extends PureComponent { +interface LogsState { + showLabels: boolean; + showLocalTime: boolean; + showUtc: boolean; +} + +export default class Logs extends PureComponent { + state = { + showLabels: true, + showLocalTime: true, + showUtc: false, + }; + + onChangeLabels = (event: React.SyntheticEvent) => { + const target = event.target as HTMLInputElement; + this.setState({ + showLabels: target.checked, + }); + }; + + onChangeLocalTime = (event: React.SyntheticEvent) => { + const target = event.target as HTMLInputElement; + this.setState({ + showLocalTime: target.checked, + }); + }; + + onChangeUtc = (event: React.SyntheticEvent) => { + const target = event.target as HTMLInputElement; + this.setState({ + showUtc: target.checked, + }); + }; + render() { - const { className = '', data, loading = false } = this.props; + const { className = '', data, loading = false, position, range } = this.props; + const { showLabels, showLocalTime, showUtc } = this.state; const hasData = data && data.rows && data.rows.length > 0; + const cssColumnSizes = ['4px']; + if (showUtc) { + cssColumnSizes.push('minmax(100px, max-content)'); + } + if (showLocalTime) { + cssColumnSizes.push('minmax(100px, max-content)'); + } + if (showLabels) { + cssColumnSizes.push('minmax(100px, 25%)'); + } + cssColumnSizes.push('1fr'); + const logEntriesStyle = { + gridTemplateColumns: cssColumnSizes.join(' '), + }; + return (
+
+ +
+ +
+
+ + + + {hasData && + data.meta && ( +
+ {data.meta.map(item => ( +
+ {item.label}: + {item.value} +
+ ))} +
+ )} +
+
+
{loading &&
} -
+
{hasData && data.rows.map(row => (
-
{row.timeLocal}
+ {showUtc &&
{row.timestamp}
} + {showLocalTime &&
{row.timeLocal}
} + {showLabels && ( +
+ {row.labels} +
+ )}
processStream(stream, DEFAULT_LIMIT)); + return { data: processedStreams }; }); } diff --git a/public/app/plugins/datasource/logging/result_transformer.test.ts b/public/app/plugins/datasource/logging/result_transformer.test.ts index c1e6913a388..28debe41585 100644 --- a/public/app/plugins/datasource/logging/result_transformer.test.ts +++ b/public/app/plugins/datasource/logging/result_transformer.test.ts @@ -1,6 +1,6 @@ import { LogLevel } from 'app/core/logs_model'; -import { getLogLevel } from './result_transformer'; +import { findCommonLabels, findUncommonLabels, formatLabels, getLogLevel, parseLabels } from './result_transformer'; describe('getLoglevel()', () => { it('returns no log level on empty line', () => { @@ -20,3 +20,57 @@ describe('getLoglevel()', () => { expect(getLogLevel('WARN this could be a debug message')).toBe(LogLevel.warn); }); }); + +describe('parseLabels()', () => { + it('returns no labels on emtpy labels string', () => { + expect(parseLabels('')).toEqual({}); + expect(parseLabels('{}')).toEqual({}); + }); + + it('returns labels on labels string', () => { + expect(parseLabels('{foo="bar", baz="42"}')).toEqual({ foo: '"bar"', baz: '"42"' }); + }); +}); + +describe('formatLabels()', () => { + it('returns no labels on emtpy label set', () => { + expect(formatLabels({})).toEqual(''); + expect(formatLabels({}, 'foo')).toEqual('foo'); + }); + + it('returns label string on label set', () => { + expect(formatLabels({ foo: '"bar"', baz: '"42"' })).toEqual('{baz="42", foo="bar"}'); + }); +}); + +describe('findCommonLabels()', () => { + it('returns no common labels on empty sets', () => { + expect(findCommonLabels([{}])).toEqual({}); + expect(findCommonLabels([{}, {}])).toEqual({}); + }); + + it('returns no common labels on differing sets', () => { + expect(findCommonLabels([{ foo: '"bar"' }, {}])).toEqual({}); + expect(findCommonLabels([{}, { foo: '"bar"' }])).toEqual({}); + expect(findCommonLabels([{ baz: '42' }, { foo: '"bar"' }])).toEqual({}); + expect(findCommonLabels([{ foo: '42', baz: '"bar"' }, { foo: '"bar"' }])).toEqual({}); + }); + + it('returns the single labels set as common labels', () => { + expect(findCommonLabels([{ foo: '"bar"' }])).toEqual({ foo: '"bar"' }); + }); +}); + +describe('findUncommonLabels()', () => { + it('returns no uncommon labels on empty sets', () => { + expect(findUncommonLabels({}, {})).toEqual({}); + }); + + it('returns all labels given no common labels', () => { + expect(findUncommonLabels({ foo: '"bar"' }, {})).toEqual({ foo: '"bar"' }); + }); + + it('returns all labels except the common labels', () => { + expect(findUncommonLabels({ foo: '"bar"', baz: '"42"' }, { foo: '"bar"' })).toEqual({ baz: '"42"' }); + }); +}); diff --git a/public/app/plugins/datasource/logging/result_transformer.ts b/public/app/plugins/datasource/logging/result_transformer.ts index 526a9c7da2c..8aa7ebc12e0 100644 --- a/public/app/plugins/datasource/logging/result_transformer.ts +++ b/public/app/plugins/datasource/logging/result_transformer.ts @@ -1,7 +1,9 @@ import _ from 'lodash'; import moment from 'moment'; -import { LogLevel, LogsModel, LogRow } from 'app/core/logs_model'; +import { LogLevel, LogsMetaItem, LogsModel, LogRow, LogsStream } from 'app/core/logs_model'; +import { TimeSeries } from 'app/core/core'; +import colors from 'app/core/utils/colors'; export function getLogLevel(line: string): LogLevel { if (!line) { @@ -19,11 +21,65 @@ export function getLogLevel(line: string): LogLevel { return level; } +const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; +export function parseLabels(labels: string): { [key: string]: string } { + const labelsByKey = {}; + labels.replace(labelRegexp, (_, key, operator, value) => { + labelsByKey[key] = value; + return ''; + }); + return labelsByKey; +} + +export function findCommonLabels(labelsSets: any[]) { + return labelsSets.reduce((acc, labels) => { + if (!labels) { + throw new Error('Need parsed labels to find common labels.'); + } + if (!acc) { + // Initial set + acc = { ...labels }; + } else { + // Remove incoming labels that are missing or not matching in value + Object.keys(labels).forEach(key => { + if (acc[key] === undefined || acc[key] !== labels[key]) { + delete acc[key]; + } + }); + // Remove common labels that are missing from incoming label set + Object.keys(acc).forEach(key => { + if (labels[key] === undefined) { + delete acc[key]; + } + }); + } + return acc; + }, undefined); +} + +export function findUncommonLabels(labels, commonLabels) { + const uncommonLabels = { ...labels }; + Object.keys(commonLabels).forEach(key => { + delete uncommonLabels[key]; + }); + return uncommonLabels; +} + +export function formatLabels(labels, defaultValue = '') { + if (!labels || Object.keys(labels).length === 0) { + return defaultValue; + } + const labelKeys = Object.keys(labels).sort(); + const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(', '); + return ['{', cleanSelector, '}'].join(''); +} + export function processEntry(entry: { line: string; timestamp: string }, stream): LogRow { const { line, timestamp } = entry; const { labels } = stream; const key = `EK${timestamp}${labels}`; const time = moment(timestamp); + const timeJs = time.valueOf(); const timeFromNow = time.fromNow(); const timeLocal = time.format('YYYY-MM-DD HH:mm:ss'); const logLevel = getLogLevel(line); @@ -32,21 +88,89 @@ export function processEntry(entry: { line: string; timestamp: string }, stream) key, logLevel, timeFromNow, + timeJs, timeLocal, entry: line, + labels: formatLabels(labels), searchWords: [stream.search], timestamp: timestamp, }; } -export function processStreams(streams, limit?: number): LogsModel { +export function mergeStreams(streams: LogsStream[], limit?: number): LogsModel { + // Find meta data + const commonLabels = findCommonLabels(streams.map(stream => stream.parsedLabels)); + const meta: LogsMetaItem[] = [ + { + label: 'Common labels', + value: formatLabels(commonLabels), + }, + ]; + + // Flatten entries of streams const combinedEntries = streams.reduce((acc, stream) => { - return [...acc, ...stream.entries.map(entry => processEntry(entry, stream))]; + // Overwrite labels to be only the non-common ones + const labels = formatLabels(findUncommonLabels(stream.parsedLabels, commonLabels)); + return [ + ...acc, + ...stream.entries.map(entry => ({ + ...entry, + labels, + })), + ]; }, []); + + const commonLabelsAlias = + streams.length === 1 ? formatLabels(commonLabels) : `Stream with common labels ${formatLabels(commonLabels)}`; + const series = streams.map((stream, index) => { + const colorIndex = index % colors.length; + stream.graphSeries.setColor(colors[colorIndex]); + stream.graphSeries.alias = formatLabels(findUncommonLabels(stream.parsedLabels, commonLabels), commonLabelsAlias); + return stream.graphSeries; + }); + const sortedEntries = _.chain(combinedEntries) .sortBy('timestamp') .reverse() .slice(0, limit || combinedEntries.length) .value(); - return { rows: sortedEntries }; + + meta.push({ + label: 'Limit', + value: `${limit} (${sortedEntries.length} returned)`, + }); + + return { meta, series, rows: sortedEntries }; +} + +export function processStream(stream: LogsStream, limit?: number): LogsStream { + const sortedEntries: any[] = _.chain(stream.entries) + .map(entry => processEntry(entry, stream)) + .sortBy('timestamp') + .reverse() + .slice(0, limit || stream.entries.length) + .value(); + + // Build graph data + let previousTime; + const datapoints = sortedEntries.reduce((acc, entry, index) => { + // Bucket to nearest minute + const time = Math.round(entry.timeJs / 1000 / 60) * 1000 * 60; + // Entry for time + if (time === previousTime) { + acc[acc.length - 1][0]++; + } else { + acc.push([1, time]); + previousTime = time; + } + return acc; + }, []); + const graphSeries = new TimeSeries({ datapoints, alias: stream.labels }); + + return { + ...stream, + graphSeries, + entries: sortedEntries, + parsedLabels: parseLabels(stream.labels), + }; } diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 0de386f3f68..6d83fc6cf0b 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -116,6 +116,11 @@ $input-border: 1px solid $input-border-color; color: $critical; } + &--small { + padding: ($input-padding-y / 2) ($input-padding-x / 2); + font-size: $font-size-xs; + } + &:disabled { color: $text-color-weak; } diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 6eb01ecc32d..c368d8ead67 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -41,7 +41,6 @@ bottom: 0; right: 0; color: #fff; - font-size: $font-size-sm; text-align: center; font-size: 150%; display: flex; @@ -91,6 +90,20 @@ transform: rotateY(0); } + &--small { + max-width: 2rem; + min-width: 1.5rem; + + input + label { + height: 25px; + } + + input + label::before, + input + label::after { + font-size: $font-size-sm; + } + } + &--table-cell { margin-bottom: 0; margin-right: 0; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index b70b058879c..70b1901fb50 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -214,7 +214,42 @@ display: grid; grid-column-gap: 1rem; grid-row-gap: 0.1rem; - grid-template-columns: 4px minmax(100px, max-content) 1fr; + grid-template-columns: 4px minmax(100px, max-content) minmax(100px, 25%) 1fr; + font-family: $font-family-monospace; + font-size: 12px; + } + + .logs-controls { + display: flex; + + > * { + margin-right: 1em; + } + } + + .logs-options, + .logs-graph { + margin-bottom: $panel-margin; + } + + .logs-meta { + flex: 1; + color: $text-color-weak; + padding: 2px 0; + } + + .logs-meta-item { + display: inline-block; + margin-right: 1em; + } + + .logs-meta-item__label { + margin-right: 0.5em; + font-size: 0.9em; + font-weight: 500; + } + + .logs-meta-item__value { font-family: $font-family-monospace; } From 6ef941ea17d75dfff6b22c5910f0c4a8489db62d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 2 Nov 2018 16:03:12 +0100 Subject: [PATCH 023/116] fix failing tests --- pkg/services/sqlstore/sqlstore.go | 1 + pkg/services/sqlstore/user.go | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 9f2d83d3284..95b53be9d4a 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -326,6 +326,7 @@ func InitTestDB(t *testing.T) *SqlStore { sqlstore := &SqlStore{} sqlstore.skipEnsureAdmin = true sqlstore.Bus = bus.New() + sqlstore.CacheService = cache.New(5*time.Minute, 10*time.Minute) dbType := migrator.SQLITE diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 8e5d61579b9..5619cd2859b 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -16,7 +16,7 @@ import ( ) func (ss *SqlStore) addUserQueryAndCommandHandlers() { - ss.Bus.AddHandler(ss.GetSignedInUser) + ss.Bus.AddHandler(ss.GetSignedInUserWithCache) bus.AddHandler("sql", GetUserById) bus.AddHandler("sql", UpdateUser) @@ -345,18 +345,24 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error { return err } -func (ss *SqlStore) GetSignedInUser(query *m.GetSignedInUserQuery) error { - orgId := "u.org_id" - if query.OrgId > 0 { - orgId = strconv.FormatInt(query.OrgId, 10) - } - - cacheKey := fmt.Sprintf("signed-in-user-%d-%s", query.UserId, query.OrgId) +func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error { + cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId) if cached, found := ss.CacheService.Get(cacheKey); found { query.Result = cached.(*m.SignedInUser) return nil } + err := GetSignedInUser(query) + ss.CacheService.Set(cacheKey, query.Result, time.Second*5) + return err +} + +func GetSignedInUser(query *m.GetSignedInUserQuery) error { + orgId := "u.org_id" + if query.OrgId > 0 { + orgId = strconv.FormatInt(query.OrgId, 10) + } + var rawSql = `SELECT u.id as user_id, u.is_admin as is_grafana_admin, @@ -407,7 +413,6 @@ func (ss *SqlStore) GetSignedInUser(query *m.GetSignedInUserQuery) error { } query.Result = &user - ss.CacheService.Set(cacheKey, &user, time.Second*5) return err } From 5803bfd2c78e702750b87923c571eadbe0fa8499 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 2 Nov 2018 17:52:40 +0100 Subject: [PATCH 024/116] fix terms agg order deprecation warning on es 6+ --- docs/sources/administration/provisioning.md | 2 +- .../features/datasources/elasticsearch.md | 2 +- .../elasticsearch/client/search_request.go | 25 +++++--- .../elasticsearch/time_series_query_test.go | 54 ++++++++++++++++ .../datasource/elasticsearch/config_ctrl.ts | 7 ++- .../datasource/elasticsearch/query_builder.ts | 13 +++- .../elasticsearch/specs/query_builder.test.ts | 62 +++++++++++++++++++ 7 files changed, 154 insertions(+), 11 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 9149aa42130..60e89b486a5 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -156,7 +156,7 @@ Since not all datasources have the same configuration settings we only have the | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | | timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source | -| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) | +| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56/60) | | timeField | string | Elasticsearch | Which field that should be used as timestamp | | interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' | | authType | string | Cloudwatch | Auth provider. keys/credentials/arn | diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 80a2f9a828a..aa60eb7cbc1 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -59,7 +59,7 @@ a time pattern for the index name or a wildcard. ### Elasticsearch version Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. -Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example. +Currently the versions available is 2.x, 5.x, 5.6+ or 6.0+. 5.6+ means a version of 5.6 or less than 6.0. 6.0+ means a version of 6.0 or higher, 6.3.2 for example. ### Min time interval A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute. diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index 4c577a2c31d..d89a98cbadb 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -112,7 +112,7 @@ func (b *SearchRequestBuilder) Query() *QueryBuilder { // Agg initiate and returns a new aggregation builder func (b *SearchRequestBuilder) Agg() AggBuilder { - aggBuilder := newAggBuilder() + aggBuilder := newAggBuilder(b.version) b.aggBuilders = append(b.aggBuilders, aggBuilder) return aggBuilder } @@ -275,11 +275,13 @@ type AggBuilder interface { type aggBuilderImpl struct { AggBuilder aggDefs []*aggDef + version int } -func newAggBuilder() *aggBuilderImpl { +func newAggBuilder(version int) *aggBuilderImpl { return &aggBuilderImpl{ aggDefs: make([]*aggDef, 0), + version: version, } } @@ -317,7 +319,7 @@ func (b *aggBuilderImpl) Histogram(key, field string, fn func(a *HistogramAgg, b }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } @@ -337,7 +339,7 @@ func (b *aggBuilderImpl) DateHistogram(key, field string, fn func(a *DateHistogr }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } @@ -347,6 +349,8 @@ func (b *aggBuilderImpl) DateHistogram(key, field string, fn func(a *DateHistogr return b } +const termsOrderTerm = "_term" + func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b AggBuilder)) AggBuilder { innerAgg := &TermsAggregation{ Field: field, @@ -358,11 +362,18 @@ func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } + if b.version >= 60 && len(innerAgg.Order) > 0 { + if orderBy, exists := innerAgg.Order[termsOrderTerm]; exists { + innerAgg.Order["_key"] = orderBy + delete(innerAgg.Order, termsOrderTerm) + } + } + b.aggDefs = append(b.aggDefs, aggDef) return b @@ -377,7 +388,7 @@ func (b *aggBuilderImpl) Filters(key string, fn func(a *FiltersAggregation, b Ag Aggregation: innerAgg, }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } @@ -398,7 +409,7 @@ func (b *aggBuilderImpl) GeoHashGrid(key, field string, fn func(a *GeoHashGridAg }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index fe8ae0fa8f2..9660d70c318 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -127,6 +127,60 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { So(avgAgg.Aggregation.Type, ShouldEqual, "avg") }) + Convey("With term agg and order by term", func() { + c := newFakeClient(5) + _, err := executeTsdbQuery(c, `{ + "timeField": "@timestamp", + "bucketAggs": [ + { + "type": "terms", + "field": "@host", + "id": "2", + "settings": { "size": "5", "order": "asc", "orderBy": "_term" } + }, + { "type": "date_histogram", "field": "@timestamp", "id": "3" } + ], + "metrics": [ + {"type": "count", "id": "1" }, + {"type": "avg", "field": "@value", "id": "5" } + ] + }`, from, to, 15*time.Second) + So(err, ShouldBeNil) + sr := c.multisearchRequests[0].Requests[0] + + firstLevel := sr.Aggs[0] + So(firstLevel.Key, ShouldEqual, "2") + termsAgg := firstLevel.Aggregation.Aggregation.(*es.TermsAggregation) + So(termsAgg.Order["_term"], ShouldEqual, "asc") + }) + + Convey("With term agg and order by term with es6.x", func() { + c := newFakeClient(60) + _, err := executeTsdbQuery(c, `{ + "timeField": "@timestamp", + "bucketAggs": [ + { + "type": "terms", + "field": "@host", + "id": "2", + "settings": { "size": "5", "order": "asc", "orderBy": "_term" } + }, + { "type": "date_histogram", "field": "@timestamp", "id": "3" } + ], + "metrics": [ + {"type": "count", "id": "1" }, + {"type": "avg", "field": "@value", "id": "5" } + ] + }`, from, to, 15*time.Second) + So(err, ShouldBeNil) + sr := c.multisearchRequests[0].Requests[0] + + firstLevel := sr.Aggs[0] + So(firstLevel.Key, ShouldEqual, "2") + termsAgg := firstLevel.Aggregation.Aggregation.(*es.TermsAggregation) + So(termsAgg.Order["_key"], ShouldEqual, "asc") + }) + Convey("With metric percentiles", func() { c := newFakeClient(5) _, err := executeTsdbQuery(c, `{ diff --git a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts index b872cc090c1..154ff9bcb91 100644 --- a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts @@ -20,7 +20,12 @@ export class ElasticConfigCtrl { { name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' }, ]; - esVersions = [{ name: '2.x', value: 2 }, { name: '5.x', value: 5 }, { name: '5.6+', value: 56 }]; + esVersions = [ + { name: '2.x', value: 2 }, + { name: '5.x', value: 5 }, + { name: '5.6+', value: 56 }, + { name: '6.0+', value: 60 }, + ]; indexPatternTypeChanged() { const def = _.find(this.indexPatternTypes, { diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.ts b/public/app/plugins/datasource/elasticsearch/query_builder.ts index a4d92397d80..21af0ba9b80 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.ts +++ b/public/app/plugins/datasource/elasticsearch/query_builder.ts @@ -31,7 +31,11 @@ export class ElasticQueryBuilder { queryNode.terms.size = parseInt(aggDef.settings.size, 10) === 0 ? 500 : parseInt(aggDef.settings.size, 10); if (aggDef.settings.orderBy !== void 0) { queryNode.terms.order = {}; - queryNode.terms.order[aggDef.settings.orderBy] = aggDef.settings.order; + if (aggDef.settings.orderBy === '_term' && this.esVersion >= 60) { + queryNode.terms.order['_key'] = aggDef.settings.order; + } else { + queryNode.terms.order[aggDef.settings.orderBy] = aggDef.settings.order; + } // if metric ref, look it up and add it to this agg level metricRef = parseInt(aggDef.settings.orderBy, 10); @@ -318,6 +322,13 @@ export class ElasticQueryBuilder { }, }, }; + + if (this.esVersion >= 60) { + query.aggs['1'].terms.order = { + _key: 'asc', + }; + } + return query; } } diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts index a9e570f366b..84929e83003 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts @@ -62,6 +62,54 @@ describe('ElasticQueryBuilder', () => { expect(aggs['1'].avg.field).toBe('@value'); }); + it('with term agg and order by term', () => { + const query = builder.build( + { + metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }], + bucketAggs: [ + { + type: 'terms', + field: '@host', + settings: { size: 5, order: 'asc', orderBy: '_term' }, + id: '2', + }, + { type: 'date_histogram', field: '@timestamp', id: '3' }, + ], + }, + 100, + 1000 + ); + + const firstLevel = query.aggs['2']; + expect(firstLevel.terms.order._term).toBe('asc'); + }); + + it('with term agg and order by term on es6.x', () => { + const builder6x = new ElasticQueryBuilder({ + timeField: '@timestamp', + esVersion: 60, + }); + const query = builder6x.build( + { + metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }], + bucketAggs: [ + { + type: 'terms', + field: '@host', + settings: { size: 5, order: 'asc', orderBy: '_term' }, + id: '2', + }, + { type: 'date_histogram', field: '@timestamp', id: '3' }, + ], + }, + 100, + 1000 + ); + + const firstLevel = query.aggs['2']; + expect(firstLevel.terms.order._key).toBe('asc'); + }); + it('with term agg and order by metric agg', () => { const query = builder.build( { @@ -302,4 +350,18 @@ describe('ElasticQueryBuilder', () => { expect(query.query.bool.filter[4].regexp['key5']).toBe('value5'); expect(query.query.bool.filter[5].bool.must_not.regexp['key6']).toBe('value6'); }); + + it('getTermsQuery should set correct sorting', () => { + const query = builder.getTermsQuery({}); + expect(query.aggs['1'].terms.order._term).toBe('asc'); + }); + + it('getTermsQuery es6.x should set correct sorting', () => { + const builder6x = new ElasticQueryBuilder({ + timeField: '@timestamp', + esVersion: 60, + }); + const query = builder6x.getTermsQuery({}); + expect(query.aggs['1'].terms.order._key).toBe('asc'); + }); }); From 5e748243af0d1a3e472fe95e02d8c6f22010debb Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sat, 3 Nov 2018 07:56:13 +0800 Subject: [PATCH 025/116] Handle suggestions for alternate syntax aggregation contexts In aggregation contexts using the alternate syntax form, labels will precede metrics. A cursor at the label position cannot provide meaningful suggestions unless a metric is specified. In the latter case, no suggestions are presented at all. Related: #13690 --- .../prometheus/language_provider.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 5f97ebfa7b5..fa9c2f1310a 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -156,7 +156,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { } getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput { - let refresher: Promise = null; + const refresher: Promise = null; const suggestions: CompletionItemGroup[] = []; // Stitch all query lines together to support multi-line queries @@ -172,12 +172,30 @@ export default class PromQlLanguageProvider extends LanguageProvider { return text; }, ''); - const leftSide = queryText.slice(0, queryOffset); - const openParensAggregationIndex = leftSide.lastIndexOf('('); - const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('('); - const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex; + // Try search for selector part on the left-hand side, such as `sum (m) by (l)` + const openParensAggregationIndex = queryText.lastIndexOf('(', queryOffset); + let openParensSelectorIndex = queryText.lastIndexOf('(', openParensAggregationIndex - 1); + let closeParensSelectorIndex = queryText.indexOf(')', openParensSelectorIndex); - let selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); + // Try search for selector part of an alternate aggregation clause, such as `sum by (l) (m)` + if (openParensSelectorIndex === -1) { + const closeParensAggregationIndex = queryText.indexOf(')', queryOffset); + closeParensSelectorIndex = queryText.indexOf(')', closeParensAggregationIndex + 1); + openParensSelectorIndex = queryText.lastIndexOf('(', closeParensSelectorIndex); + } + + const result = { + refresher, + suggestions, + context: 'context-aggregation', + }; + + // Suggestions are useless for alternative aggregation clauses without a selector in context + if (openParensSelectorIndex === -1) { + return result; + } + + let selectorString = queryText.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); // Range vector syntax not accounted for by subsequent parse so discard it if present selectorString = selectorString.replace(/\[[^\]]+\]$/, ''); @@ -188,14 +206,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { if (labelKeys) { suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); } else { - refresher = this.fetchSeriesLabels(selector); + result.refresher = this.fetchSeriesLabels(selector); } - return { - refresher, - suggestions, - context: 'context-aggregation', - }; + return result; } getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput { From f79b790ef68cb05f5ac6373c8c75aa99fec0b366 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sat, 3 Nov 2018 08:07:08 +0800 Subject: [PATCH 026/116] Add tests covering alternate syntax for aggregation contexts Related: #13690 --- .../specs/language_provider.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 20e148efd57..784a8b59739 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -269,5 +269,48 @@ describe('Language completion provider', () => { }, ]); }); + + it('returns no suggestions inside an unclear aggregation context using alternate syntax', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] }, + }); + const value = Plain.deserialize('sum by ()'); + const range = value.selection.merge({ + anchorOffset: 8, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([]); + }); + + it('returns label suggestions inside an aggregation context using alternate syntax', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] }, + }); + const value = Plain.deserialize('sum by () (metric)'); + const range = value.selection.merge({ + anchorOffset: 8, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([ + { + items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }], + label: 'Labels', + }, + ]); + }); }); }); From d6cd2a208573e051d37d497ea9368b8b5e1c4ac5 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Sun, 4 Nov 2018 00:48:30 -0700 Subject: [PATCH 027/116] Gitlab -> GitLab --- docs/sources/guides/whats-new-in-v5-3.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-3.md b/docs/sources/guides/whats-new-in-v5-3.md index 5dcadc0813d..10592f51648 100644 --- a/docs/sources/guides/whats-new-in-v5-3.md +++ b/docs/sources/guides/whats-new-in-v5-3.md @@ -18,7 +18,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article - [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible - [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders - [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder! -- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for Gitlab is improved +- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for GitLab is improved - [Annotations]({{< relref "#annotations" >}}) with template variable filtering - [Variables]({{< relref "#variables" >}}) with free text support @@ -69,9 +69,9 @@ Grafana 5.3 comes with a new graphical query builder for Postgres. This brings P {{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}} -## Improved OAuth Support for Gitlab +## Improved OAuth Support for GitLab -Grafana 5.3 comes with a new OAuth integration for Gitlab that enables configuration to only allow users that are a member of certain Gitlab groups to authenticate. This makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving everyone access to Grafana. +Grafana 5.3 comes with a new OAuth integration for GitLab that enables configuration to only allow users that are a member of certain GitLab groups to authenticate. This makes it possible to use GitLab OAuth with Grafana in a shared environment without giving everyone access to Grafana. Learn how to enable and configure it in the [documentation](/auth/gitlab/). ## Annotations From 355493bf6ee77f5d31ae8e3b377ea91f38da065d Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Sun, 4 Nov 2018 03:22:25 -0800 Subject: [PATCH 028/116] typo fix for "has" --- public/app/partials/reset_password.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/partials/reset_password.html b/public/app/partials/reset_password.html index 92ed91d5f4d..138aa1b7c62 100644 --- a/public/app/partials/reset_password.html +++ b/public/app/partials/reset_password.html @@ -19,7 +19,7 @@
- An email with a reset link as been sent to the email address.
+ An email with a reset link has been sent to the email address.
You should receive it shortly.
From cb2a03a08c2032a28df000396472cd03cbb1febe Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Mon, 5 Nov 2018 14:27:27 +0900 Subject: [PATCH 029/116] add minimal permission --- docs/sources/features/datasources/cloudwatch.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index be36d108475..e2bcb50bb1d 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -60,7 +60,8 @@ Here is a minimal policy example: "Effect": "Allow", "Action": [ "cloudwatch:ListMetrics", - "cloudwatch:GetMetricStatistics" + "cloudwatch:GetMetricStatistics", + "cloudwatch:GetMetricData" ], "Resource": "*" }, From 7c3dcb3702268e7b0a9055618a53f3ab43897ae2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 09:03:35 +0100 Subject: [PATCH 030/116] alerting: adds tests for the median reducer adds a test that verify that null values are not used when calculating the median value in alerting closes #10056 --- .../alerting/conditions/reducer_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 9d4e1462690..7f11fc498bd 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -52,6 +52,24 @@ func TestSimpleReducer(t *testing.T) { So(result, ShouldEqual, float64(1)) }) + Convey("median should ignore null values", func() { + reducer := NewSimpleReducer("median") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 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(float64(1)), 4)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(2)), 5)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(3)), 6)) + + result := reducer.Reduce(series) + So(result.Valid, ShouldEqual, true) + So(result.Float64, ShouldEqual, float64(2)) + }) + Convey("avg", func() { result := testReducer("avg", 1, 2, 3) So(result, ShouldEqual, float64(2)) From 5469a1a56951061fed8f5e4db3473f6a070e824e Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 5 Nov 2018 09:55:19 +0100 Subject: [PATCH 031/116] build: fixes gcp push path. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 60b3ae91ccc..af5acef4bea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -403,7 +403,7 @@ jobs: command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json' - run: name: deploy to gcp - command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://R/oss/release' + command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/release' - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh' From 5be2332c668ab459d7f0eadd16ece31b02fa193d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Nov 2018 09:58:13 +0100 Subject: [PATCH 032/116] handle error before populating cache --- pkg/services/sqlstore/user.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 5619cd2859b..6e4b12ca7c3 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -353,8 +353,12 @@ func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) erro } err := GetSignedInUser(query) + if err != nil { + return err + } + ss.CacheService.Set(cacheKey, query.Result, time.Second*5) - return err + return nil } func GetSignedInUser(query *m.GetSignedInUserQuery) error { From 818d48c2c06e884a3e6e6961ca25b07af6e5aba6 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Nov 2018 10:49:56 +0100 Subject: [PATCH 033/116] always execute the user teams query --- pkg/services/sqlstore/user.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 6e4b12ca7c3..99a77ecabc3 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -403,17 +403,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { if user.OrgRole == "" { user.OrgId = -1 user.OrgName = "Org missing" - } else { - getTeamsByUserQuery := &m.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId} - err = GetTeamsByUser(getTeamsByUserQuery) - if err != nil { - return err - } + } - user.Teams = make([]int64, len(getTeamsByUserQuery.Result)) - for i, t := range getTeamsByUserQuery.Result { - user.Teams[i] = t.Id - } + getTeamsByUserQuery := &m.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId} + err = GetTeamsByUser(getTeamsByUserQuery) + if err != nil { + return err + } + + user.Teams = make([]int64, len(getTeamsByUserQuery.Result)) + for i, t := range getTeamsByUserQuery.Result { + user.Teams[i] = t.Id } query.Result = &user From 32c93793a628c8e31b6252ebceff60665d927414 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Nov 2018 11:34:25 +0100 Subject: [PATCH 034/116] devenv: table panel links --- devenv/dev-dashboards/panel_tests_table.json | 110 ++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_table.json b/devenv/dev-dashboards/panel_tests_table.json index 8337e9cd746..ff0288c340a 100644 --- a/devenv/dev-dashboards/panel_tests_table.json +++ b/devenv/dev-dashboards/panel_tests_table.json @@ -404,6 +404,112 @@ "title": "Column style thresholds & units", "transform": "timeseries_to_columns", "type": "table" + }, + { + "columns": [], + "datasource": "gdev-testdata", + "fontSize": "100%", + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 6, + "links": [], + "pageSize": 20, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkTargetBlank": true, + "linkTooltip": "", + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "ColorValue", + "expr": "", + "format": "table", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "null,1,20,90,30,5,0,20,10" + }, + { + "alias": "ColorCell", + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "null,5,1,2,3,4,5,10,20" + } + ], + "title": "Column style thresholds and links", + "transform": "timeseries_to_columns", + "type": "table" } ], "refresh": false, @@ -449,5 +555,5 @@ "timezone": "browser", "title": "Panel Tests - Table", "uid": "pttable", - "version": 1 -} + "version": 2 +} \ No newline at end of file From c5ce8536d4de872077116523f77a404273115cc7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Nov 2018 11:47:34 +0100 Subject: [PATCH 035/116] changelog: add notes about closing #13606 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97537ec34f7..d139009ac78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) * **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876) * **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945) +* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme) ### Breaking changes From dd4eab17222dcc8e3efc74d66a71e3845ad1e658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Nov 2018 13:44:09 +0100 Subject: [PATCH 036/116] panel options wip --- public/app/plugins/panel/graph2/module.tsx | 27 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 4011458bea9..7bb2ab48e40 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -12,6 +12,10 @@ import { PanelProps, NullValueMode } from 'app/types'; interface Options { showBars: boolean; + showLines: boolean; + showPoints: boolean; + + onChange: (options: Options) => void; } interface Props extends PanelProps { @@ -35,14 +39,27 @@ export class Graph2 extends PureComponent { } } -export class TextOptions extends PureComponent { - onChange = () => {}; +export class TextOptions extends PureComponent { + onToggleLines = () => { + const options = this.props as Options; + + this.props.onChange({ + ...options, + showLines: !this.props.showLines, + }); + }; render() { + const { showBars, showPoints, showLines } = this.props; + return ( -
-
Draw Modes
- +
+
+
Draw Modes
+ + + +
); } From 9e0da02b6a5974cb74d230df2f7e168e46144028 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Nov 2018 14:25:19 +0100 Subject: [PATCH 037/116] refactor dashboard alert extractor --- pkg/api/alerting.go | 4 ++++ pkg/models/alert.go | 3 ++- pkg/services/alerting/commands.go | 6 +++--- pkg/services/alerting/extractor.go | 19 ++++++++++++++++++- pkg/services/alerting/extractor_test.go | 16 ++++++++-------- pkg/services/alerting/test_rule.go | 3 ++- pkg/services/dashboards/dashboard_service.go | 3 ++- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index a936d696207..c68cee50948 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -134,12 +134,16 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response { OrgId: c.OrgId, Dashboard: dto.Dashboard, PanelId: dto.PanelId, + User: c.SignedInUser, } if err := bus.Dispatch(&backendCmd); err != nil { if validationErr, ok := err.(alerting.ValidationError); ok { return Error(422, validationErr.Error(), nil) } + if err == m.ErrDataSourceAccessDenied { + return Error(403, "Access denied to datasource", err) + } return Error(500, "Failed to test rule", err) } diff --git a/pkg/models/alert.go b/pkg/models/alert.go index ba1fc0779ba..aaf9c50197a 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -215,13 +215,14 @@ type AlertStateInfoDTO struct { // "Internal" commands type UpdateDashboardAlertsCommand struct { - UserId int64 OrgId int64 Dashboard *Dashboard + User *SignedInUser } type ValidateDashboardAlertsCommand struct { UserId int64 OrgId int64 Dashboard *Dashboard + User *SignedInUser } diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 02186d697ee..dd2ff5658d6 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -11,7 +11,7 @@ func init() { } func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error { - extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) + extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User) return extractor.ValidateAlerts() } @@ -19,11 +19,11 @@ func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error { func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error { saveAlerts := m.SaveAlertsCommand{ OrgId: cmd.OrgId, - UserId: cmd.UserId, + UserId: cmd.User.UserId, DashboardId: cmd.Dashboard.Id, } - extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) + extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User) alerts, err := extractor.GetAlerts() if err != nil { diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index edfab2dedee..0abacc91313 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -13,14 +13,16 @@ import ( // DashAlertExtractor extracts alerts from the dashboard json type DashAlertExtractor struct { + User *m.SignedInUser Dash *m.Dashboard OrgID int64 log log.Logger } // NewDashAlertExtractor returns a new DashAlertExtractor -func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor { +func NewDashAlertExtractor(dash *m.Dashboard, orgID int64, user *m.SignedInUser) *DashAlertExtractor { return &DashAlertExtractor{ + User: user, Dash: dash, OrgID: orgID, log: log.New("alerting.extractor"), @@ -149,6 +151,21 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)} } + dsFilterQuery := m.DatasourcesPermissionFilterQuery{ + User: e.User, + Datasources: []*m.DataSource{datasource}, + } + + if err := bus.Dispatch(&dsFilterQuery); err != nil { + if err != bus.ErrHandlerNotFound { + return nil, err + } + } else { + if len(dsFilterQuery.Result) == 0 { + return nil, m.ErrDataSourceAccessDenied + } + } + jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) if interval, err := panel.Get("interval").String(); err == nil { diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index e2dc01a1181..0890b9e1bd1 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -69,7 +69,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(getTarget(dashJson), ShouldEqual, "") }) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, _ = extractor.GetAlerts() Convey("Dashboard json should not be updated after extracting rules", func() { @@ -83,7 +83,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -146,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJson, err := simplejson.NewJson(panelWithoutId) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, err = extractor.GetAlerts() @@ -162,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJson, err := simplejson.NewJson(panelWithIdZero) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, err = extractor.GetAlerts() @@ -178,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJson, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -198,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJson, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -228,7 +228,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -248,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) err = extractor.ValidateAlerts() diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index 88418bff14e..360ee065de0 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -13,6 +13,7 @@ type AlertTestCommand struct { Dashboard *simplejson.Json PanelId int64 OrgId int64 + User *m.SignedInUser Result *EvalContext } @@ -25,7 +26,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error { dash := m.NewDashboardFromJson(cmd.Dashboard) - extractor := NewDashAlertExtractor(dash, cmd.OrgId) + extractor := NewDashAlertExtractor(dash, cmd.OrgId, cmd.User) alerts, err := extractor.GetAlerts() if err != nil { return err diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 8eb7f4a6e72..b52d1845a0b 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -90,6 +90,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlertsCmd := models.ValidateDashboardAlertsCommand{ OrgId: dto.OrgId, Dashboard: dash, + User: dto.User, } if err := bus.Dispatch(&validateAlertsCmd); err != nil { @@ -159,8 +160,8 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error { alertCmd := models.UpdateDashboardAlertsCommand{ OrgId: dto.OrgId, - UserId: dto.User.UserId, Dashboard: cmd.Result, + User: dto.User, } if err := bus.Dispatch(&alertCmd); err != nil { From 423331dae03e9abe33c6656d72d6185dd5a9bd06 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 14:24:08 +0100 Subject: [PATCH 038/116] alerting: delete alerts when parent folder is deleted closes #13322 --- pkg/services/sqlstore/dashboard.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 1b853d17b5f..bad46c10af4 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -327,6 +327,24 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { if dashboard.IsFolder { deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)") deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?") + + dashIds := []struct { + Id int64 + }{} + err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds) + if err != nil { + return err + } + + for _, id := range dashIds { + if err := deleteAlertDefinition(id.Id, sess); err != nil { + return nil + } + } + } + + if err := deleteAlertDefinition(dashboard.Id, sess); err != nil { + return nil } for _, sql := range deletes { @@ -337,10 +355,6 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { } } - if err := deleteAlertDefinition(dashboard.Id, sess); err != nil { - return nil - } - return nil }) } From a1dca2117ddf6346b742da85b9c4b2218449d64e Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 5 Nov 2018 15:05:12 +0100 Subject: [PATCH 039/116] build: use build workflow id instead of build number. (#13965) * build: use build workflow id instead of build number. The workflow id is unique across the whole workflow while the build number is unique to every job in the workflow. This change means that jobs that build artifacts for the same commit but in different jobs will now have the same id. * build: fixes pkgver generation. --- build.go | 28 ++++++++++++++++++++-------- scripts/build/build-all.sh | 4 ++-- scripts/build/build.sh | 4 ++-- scripts/build/deploy.sh | 14 -------------- 4 files changed, 24 insertions(+), 26 deletions(-) delete mode 100755 scripts/build/deploy.sh diff --git a/build.go b/build.go index b136754efbc..a2a1fb825d9 100644 --- a/build.go +++ b/build.go @@ -41,8 +41,8 @@ var ( race bool phjsToRelease string workingDir string - includeBuildNumber bool = true - buildNumber int = 0 + includeBuildId bool = true + buildId string = "0" binaries []string = []string{"grafana-server", "grafana-cli"} isDev bool = false enterprise bool = false @@ -54,6 +54,8 @@ func main() { ensureGoPath() + var buildIdRaw string + flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH") flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS") flag.StringVar(&gocc, "cc", "", "CC") @@ -61,12 +63,14 @@ func main() { flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH") flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary") flag.BoolVar(&race, "race", race, "Use race detector") - flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name") + flag.BoolVar(&includeBuildId, "includeBuildId", includeBuildId, "IncludeBuildId in package name") flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana") - flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system") + flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system") flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps") flag.Parse() + buildId = shortenBuildId(buildIdRaw) + readVersionFromPackageJson() if pkgArch == "" { @@ -197,9 +201,9 @@ func readVersionFromPackageJson() { } // add timestamp to iteration - if includeBuildNumber { - if buildNumber != 0 { - linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration) + if includeBuildId { + if buildId != "0" { + linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration) } else { linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration) } @@ -392,7 +396,7 @@ func grunt(params ...string) { func gruntBuildArg(task string) []string { args := []string{task} - if includeBuildNumber { + if includeBuildId { args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)) } else { args = append(args, fmt.Sprintf("--pkgVer=%v", version)) @@ -632,3 +636,11 @@ func shaFile(file string) error { return out.Close() } + +func shortenBuildId(buildId string) string { + buildId = strings.Replace(buildId, "-", "", -1) + if (len(buildId) < 9) { + return buildId + } + return buildId[0:8] +} diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh index f194109ec0d..3e7058fa494 100755 --- a/scripts/build/build-all.sh +++ b/scripts/build/build-all.sh @@ -22,10 +22,10 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Building releases from tag $CIRCLE_TAG" - OPT="-includeBuildNumber=false ${EXTRA_OPTS}" + OPT="-includeBuildId=false ${EXTRA_OPTS}" else echo "Building incremental build for $CIRCLE_BRANCH" - OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}" + OPT="-buildId=${CIRCLE_WORKFLOW_ID} ${EXTRA_OPTS}" fi echo "Build arguments: $OPT" diff --git a/scripts/build/build.sh b/scripts/build/build.sh index 2cf9f5a8a21..8362942c6cd 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -18,10 +18,10 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Building releases from tag $CIRCLE_TAG" - OPT="-includeBuildNumber=false ${EXTRA_OPTS}" + OPT="-includeBuildId=false ${EXTRA_OPTS}" else echo "Building incremental build for $CIRCLE_BRANCH" - OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}" + OPT="-buildId=${CIRCLE_WORKFLOW_ID} ${EXTRA_OPTS}" fi echo "Build arguments: $OPT" diff --git a/scripts/build/deploy.sh b/scripts/build/deploy.sh deleted file mode 100755 index 49b2a9e3a7c..00000000000 --- a/scripts/build/deploy.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -mkdir -p dist - -echo "Circle branch: ${CIRCLE_BRANCH}" -echo "Circle tag: ${CIRCLE_TAG}" -docker run -i -t --name gfbuild \ - -v $(pwd):/go/src/github.com/grafana/grafana \ - -e "CIRCLE_BRANCH=${CIRCLE_BRANCH}" \ - -e "CIRCLE_TAG=${CIRCLE_TAG}" \ - -e "CIRCLE_BUILD_NUM=${CIRCLE_BUILD_NUM}" \ - grafana/buildcontainer - -sudo chown -R ${USER:=$(/usr/bin/id -run)}:$USER dist From 7e093a32a282f25b4922c856d9cf21e12689c7ad Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 31 Oct 2018 16:59:30 +0100 Subject: [PATCH 040/116] build: improved release publisher dry-run. --- scripts/build/release_publisher/main.go | 11 +++-- scripts/build/release_publisher/publisher.go | 46 ++++++++++---------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go index fde4317bbb9..40430aebf2b 100644 --- a/scripts/build/release_publisher/main.go +++ b/scripts/build/release_publisher/main.go @@ -7,8 +7,6 @@ import ( "os" ) -var baseUri string = "https://grafana.com/api" - func main() { var version string var whatsNewUrl string @@ -33,8 +31,13 @@ func main() { log.Println("Dry-run has been enabled.") } - p := publisher{apiKey: apiKey} - if err := p.doRelease(version, whatsNewUrl, releaseNotesUrl, dryRun); err != nil { + p := publisher{ + apiKey: apiKey, + baseUri: "https://grafana.com/api", + product: "grafana", + dryRun: dryRun, + } + if err := p.doRelease(version, whatsNewUrl, releaseNotesUrl); err != nil { log.Fatalf("error: %v", err) } } diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index 60b60ca55f7..0466aaa5687 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -13,52 +13,39 @@ import ( type publisher struct { apiKey string + baseUri string + product string + dryRun bool } -func (p *publisher) doRelease(version string, whatsNewUrl string, releaseNotesUrl string, dryRun bool) error { +func (p *publisher) doRelease(version string, whatsNewUrl string, releaseNotesUrl string) error { currentRelease, err := newRelease(version, whatsNewUrl, releaseNotesUrl, buildArtifactConfigurations, getHttpContents{}) if err != nil { return err } - if dryRun { - relJson, err := json.Marshal(currentRelease) - if err != nil { - return err - } - log.Println(string(relJson)) - - for _, b := range currentRelease.Builds { - artifactJson, err := json.Marshal(b) - if err != nil { - return err - } - log.Println(string(artifactJson)) - } - } else { - if err := p.postRelease(currentRelease); err != nil { - return err - } + if err := p.postRelease(currentRelease); err != nil { + return err } return nil } func (p *publisher) postRelease(r *release) error { - err := p.postRequest("/grafana/versions", r, fmt.Sprintf("Create Release %s", r.Version)) + err := p.postRequest("/versions", r, fmt.Sprintf("Create Release %s", r.Version)) if err != nil { return err } - err = p.postRequest("/grafana/versions/"+r.Version, r, fmt.Sprintf("Update Release %s", r.Version)) + err = p.postRequest("/versions/"+r.Version, r, fmt.Sprintf("Update Release %s", r.Version)) if err != nil { return err } for _, b := range r.Builds { - err = p.postRequest(fmt.Sprintf("/grafana/versions/%s/packages", r.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch)) + err = p.postRequest(fmt.Sprintf("/versions/%s/packages", r.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch)) if err != nil { return err } - err = p.postRequest(fmt.Sprintf("/grafana/versions/%s/packages/%s/%s", r.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch)) + err = p.postRequest(fmt.Sprintf("/versions/%s/packages/%s/%s", r.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch)) if err != nil { return err } @@ -185,12 +172,23 @@ func newBuild(ba buildArtifact, version string, isBeta bool, sha256 string) buil } } +func (p *publisher) apiUrl(url string) string { + return fmt.Sprintf("%s/%s%s", p.baseUri, p.product, url) +} + func (p *publisher) postRequest(url string, obj interface{}, desc string) error { jsonBytes, err := json.Marshal(obj) if err != nil { return err } - req, err := http.NewRequest(http.MethodPost, baseUri+url, bytes.NewReader(jsonBytes)) + + if p.dryRun { + log.Println(fmt.Sprintf("POST to %s:", p.apiUrl(url))) + log.Println(string(jsonBytes)) + return nil + } + + req, err := http.NewRequest(http.MethodPost, p.apiUrl(url), bytes.NewReader(jsonBytes)) if err != nil { return err } From e2d3382470a3bc3cf536254cf860907ce3fa727a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 1 Nov 2018 13:35:31 +0100 Subject: [PATCH 041/116] build: prepares release tool for finding local releases. --- scripts/build/release_publisher/main.go | 29 +++++++++---- scripts/build/release_publisher/publisher.go | 42 ++++++++++++------- .../build/release_publisher/publisher_test.go | 9 +++- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go index 40430aebf2b..8b3ec8da74e 100644 --- a/scripts/build/release_publisher/main.go +++ b/scripts/build/release_publisher/main.go @@ -12,6 +12,7 @@ func main() { var whatsNewUrl string var releaseNotesUrl string var dryRun bool + var enterprise bool var apiKey string flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)") @@ -19,25 +20,39 @@ func main() { flag.StringVar(&releaseNotesUrl, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)") flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)") flag.BoolVar(&dryRun, "dry-run", false, "--dry-run") + flag.BoolVar(&enterprise, "enterprise", false, "--enterprise") flag.Parse() if len(os.Args) == 1 { - fmt.Println("Usage: go run publisher.go main.go --version --wn --rn --apikey --dry-run false") - fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run true") + fmt.Println("Usage: go run publisher.go main.go --version --wn --rn --apikey --dry-run false --enterprise false") + fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run --enterprise") os.Exit(1) } if dryRun { log.Println("Dry-run has been enabled.") } + var baseUrl string + + if enterprise { + baseUrl = fmt.Sprintf("https://s3-us-west-2.amazonaws.com/%s", "grafana-enterprise-releases/release/grafana-enterprise") + } else { + baseUrl = fmt.Sprintf("https://s3-us-west-2.amazonaws.com/%s", "grafana-releases/release/grafana") + } p := publisher{ - apiKey: apiKey, - baseUri: "https://grafana.com/api", - product: "grafana", - dryRun: dryRun, + apiKey: apiKey, + baseUri: "https://grafana.com/api", + product: "grafana", + dryRun: dryRun, + enterprise: enterprise, + baseArchiveUrl: baseUrl, + builder: releaseFromExternalContent{ + getter: getHttpContents{}, + rawVersion: version, + }, } - if err := p.doRelease(version, whatsNewUrl, releaseNotesUrl); err != nil { + if err := p.doRelease(whatsNewUrl, releaseNotesUrl); err != nil { log.Fatalf("error: %v", err) } } diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index 0466aaa5687..2bd84462daa 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -12,14 +12,21 @@ import ( ) type publisher struct { - apiKey string - baseUri string - product string - dryRun bool + apiKey string + baseUri string + product string + dryRun bool + enterprise bool + baseArchiveUrl string + builder releaseBuilder } -func (p *publisher) doRelease(version string, whatsNewUrl string, releaseNotesUrl string) error { - currentRelease, err := newRelease(version, whatsNewUrl, releaseNotesUrl, buildArtifactConfigurations, getHttpContents{}) +type releaseBuilder interface { + prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact) (*release, error) +} + +func (p *publisher) doRelease(whatsNewUrl string, releaseNotesUrl string) error { + currentRelease, err := p.builder.prepareRelease(p.baseArchiveUrl, whatsNewUrl, releaseNotesUrl, buildArtifactConfigurations) if err != nil { return err } @@ -54,15 +61,13 @@ func (p *publisher) postRelease(r *release) error { return nil } -const baseArhiveUrl = "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana" - type buildArtifact struct { os string arch string urlPostfix string } -func (t buildArtifact) getUrl(version string, isBeta bool) string { +func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) string { prefix := "-" rhelReleaseExtra := "" @@ -74,7 +79,7 @@ func (t buildArtifact) getUrl(version string, isBeta bool) string { rhelReleaseExtra = "-1" } - url := strings.Join([]string{baseArhiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "") + url := strings.Join([]string{baseArchiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "") return url } @@ -136,18 +141,23 @@ var buildArtifactConfigurations = []buildArtifact{ }, } -func newRelease(rawVersion string, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact, getter urlGetter) (*release, error) { - version := rawVersion[1:] +type releaseFromExternalContent struct { + getter urlGetter + rawVersion string +} + +func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact) (*release, error) { + version := re.rawVersion[1:] now := time.Now() isBeta := strings.Contains(version, "beta") builds := []build{} for _, ba := range artifactConfigurations { - sha256, err := getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(version, isBeta))) + sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, isBeta))) if err != nil { return nil, err } - builds = append(builds, newBuild(ba, version, isBeta, sha256)) + builds = append(builds, newBuild(baseArchiveUrl, ba, version, isBeta, sha256)) } r := release{ @@ -163,10 +173,10 @@ func newRelease(rawVersion string, whatsNewUrl string, releaseNotesUrl string, a return &r, nil } -func newBuild(ba buildArtifact, version string, isBeta bool, sha256 string) build { +func newBuild(baseArchiveUrl string, ba buildArtifact, version string, isBeta bool, sha256 string) build { return build{ Os: ba.os, - Url: ba.getUrl(version, isBeta), + Url: ba.getUrl(baseArchiveUrl, version, isBeta), Sha256: sha256, Arch: ba.arch, } diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index 9bc350e6a54..38a9d972380 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -9,9 +9,14 @@ func TestNewRelease(t *testing.T) { relNotesUrl := "https://relnotes.foo/" expectedArch := "amd64" expectedOs := "linux" - buildArtifacts := []buildArtifact{{expectedOs, expectedArch, ".linux-amd64.tar.gz"}} + buildArtifacts := []buildArtifact{{expectedOs,expectedArch, ".linux-amd64.tar.gz"}} - rel, _ := newRelease(versionIn, whatsNewUrl, relNotesUrl, buildArtifacts, mockHttpGetter{}) + builder := releaseFromExternalContent{ + getter: mockHttpGetter{}, + rawVersion: versionIn, + } + + rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl, buildArtifacts) if !rel.Beta || rel.Stable { t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn) From c5c3e08442fb2f8cb3eedceb4fc30ff7708b67ab Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 1 Nov 2018 14:49:32 +0100 Subject: [PATCH 042/116] build: refactor releaser. --- .../release_publisher/externalrelease.go | 62 +++++++++++++++++++ scripts/build/release_publisher/publisher.go | 53 ---------------- .../build/release_publisher/publisher_test.go | 7 ++- 3 files changed, 68 insertions(+), 54 deletions(-) create mode 100644 scripts/build/release_publisher/externalrelease.go diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go new file mode 100644 index 00000000000..795e3bc999b --- /dev/null +++ b/scripts/build/release_publisher/externalrelease.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" +) + +type releaseFromExternalContent struct { + getter urlGetter + rawVersion string +} + +func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact) (*release, error) { + version := re.rawVersion[1:] + now := time.Now() + isBeta := strings.Contains(version, "beta") + + builds := []build{} + for _, ba := range artifactConfigurations { + sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, isBeta))) + if err != nil { + return nil, err + } + builds = append(builds, newBuild(baseArchiveUrl, ba, version, isBeta, sha256)) + } + + r := release{ + Version: version, + ReleaseDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local), + Stable: !isBeta, + Beta: isBeta, + Nightly: false, + WhatsNewUrl: whatsNewUrl, + ReleaseNotesUrl: releaseNotesUrl, + Builds: builds, + } + return &r, nil +} + +type urlGetter interface { + getContents(url string) (string, error) +} + +type getHttpContents struct{} + +func (getHttpContents) getContents(url string) (string, error) { + response, err := http.Get(url) + if err != nil { + return "", err + } + + defer response.Body.Close() + all, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", err + } + + return string(all), nil +} diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index 2bd84462daa..e8c3b676e59 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -141,38 +141,6 @@ var buildArtifactConfigurations = []buildArtifact{ }, } -type releaseFromExternalContent struct { - getter urlGetter - rawVersion string -} - -func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact) (*release, error) { - version := re.rawVersion[1:] - now := time.Now() - isBeta := strings.Contains(version, "beta") - - builds := []build{} - for _, ba := range artifactConfigurations { - sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, isBeta))) - if err != nil { - return nil, err - } - builds = append(builds, newBuild(baseArchiveUrl, ba, version, isBeta, sha256)) - } - - r := release{ - Version: version, - ReleaseDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local), - Stable: !isBeta, - Beta: isBeta, - Nightly: false, - WhatsNewUrl: whatsNewUrl, - ReleaseNotesUrl: releaseNotesUrl, - Builds: builds, - } - return &r, nil -} - func newBuild(baseArchiveUrl string, ba buildArtifact, version string, isBeta bool, sha256 string) build { return build{ Os: ba.os, @@ -251,24 +219,3 @@ type build struct { Sha256 string `json:"sha256"` Arch string `json:"arch"` } - -type urlGetter interface { - getContents(url string) (string, error) -} - -type getHttpContents struct{} - -func (getHttpContents) getContents(url string) (string, error) { - response, err := http.Get(url) - if err != nil { - return "", err - } - - defer response.Body.Close() - all, err := ioutil.ReadAll(response.Body) - if err != nil { - return "", err - } - - return string(all), nil -} diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index 38a9d972380..04a9c0ca54c 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -2,7 +2,7 @@ package main import "testing" -func TestNewRelease(t *testing.T) { +func TestPreparingReleaseFromRemote(t *testing.T) { versionIn := "v5.2.0-beta1" expectedVersion := "5.2.0-beta1" whatsNewUrl := "https://whatsnews.foo/" @@ -46,3 +46,8 @@ type mockHttpGetter struct{} func (mockHttpGetter) getContents(url string) (string, error) { return url, nil } + + +func TestPreparingReleaseFromLocal(t *testing.T) { + +} From d9eaec99e2f04eea03fa9ab0a0971d9d1290f91a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 2 Nov 2018 14:56:46 +0100 Subject: [PATCH 043/116] build: publisher can find artifacts from local sources. --- ...nterprise-5.4.0-123pre1.linux-amd64.tar.gz | 0 ...se-5.4.0-123pre1.linux-amd64.tar.gz.sha256 | 1 + ...enterprise-5.4.0-123pre1.windows-amd64.zip | 0 ...ise-5.4.0-123pre1.windows-amd64.zip.sha256 | 1 + ...rafana-enterprise-5.4.0-123pre1.x86_64.rpm | 0 ...enterprise-5.4.0-123pre1.x86_64.rpm.sha256 | 1 + ...grafana-enterprise_5.4.0-123pre1_amd64.deb | 0 ...-enterprise_5.4.0-123pre1_amd64.deb.sha256 | 1 + .../build/release_publisher/localrelease.go | 91 +++++++++++++++++++ .../build/release_publisher/publisher_test.go | 59 +++++++++++- 10 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz create mode 100644 scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 create mode 100644 scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip create mode 100644 scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 create mode 100644 scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm create mode 100644 scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 create mode 100644 scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb create mode 100644 scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 create mode 100644 scripts/build/release_publisher/localrelease.go diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 new file mode 100644 index 00000000000..c3068040269 --- /dev/null +++ b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 @@ -0,0 +1 @@ +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 new file mode 100644 index 00000000000..c3068040269 --- /dev/null +++ b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 @@ -0,0 +1 @@ +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 new file mode 100644 index 00000000000..c3068040269 --- /dev/null +++ b/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 @@ -0,0 +1 @@ +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb b/scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 b/scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 new file mode 100644 index 00000000000..c3068040269 --- /dev/null +++ b/scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 @@ -0,0 +1 @@ +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go new file mode 100644 index 00000000000..45bebc524e4 --- /dev/null +++ b/scripts/build/release_publisher/localrelease.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "github.com/pkg/errors" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "time" +) + +type releaseLocalSources struct { + path string +} + +func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact) (*release, error) { + buildData := r.findBuilds(artifactConfigurations, baseArchiveUrl) + + rel := release{ + Version: buildData.version, + ReleaseDate: time.Time{}, + Stable: false, + Beta: false, + Nightly: true, + WhatsNewUrl: whatsNewUrl, + ReleaseNotesUrl: releaseNotesUrl, + Builds: buildData.builds, + } + + return &rel, nil +} + +type buildData struct { + version string + builds []build +} + +func (r releaseLocalSources) findBuilds(buildArtifacts []buildArtifact, baseArchiveUrl string) buildData { + data := buildData{} + filepath.Walk(r.path, createBuildWalker(r.path, &data, buildArtifacts, baseArchiveUrl)) + return data +} + +func createBuildWalker(path string, data *buildData, archiveTypes []buildArtifact, baseArchiveUrl string) func(path string, f os.FileInfo, err error) error { + return func(path string, f os.FileInfo, err error) error { + if err != nil { + log.Printf("error: %v", err) + } + + if f.Name() == path || strings.HasSuffix(f.Name(), ".sha256") { + return nil + } + + shaBytes, err := ioutil.ReadFile(path + ".sha256") + if err != nil { + log.Fatalf("Failed to read sha256 file %v", err) + } + + + for _, archive := range archiveTypes { + if strings.HasSuffix(f.Name(), archive.urlPostfix) { + version, err := grabVersion(f.Name(), archive.urlPostfix) + if err != nil { + log.Println(err) + continue + } + data.version = version + data.builds = append(data.builds, build{ + Os: archive.os, + Url: archive.getUrl(baseArchiveUrl, version, false), + Sha256: string(shaBytes), + Arch: archive.arch, + }) + return nil + } + } + return nil + } + +} +func grabVersion(name string, suffix string) (string, error) { + match := regexp.MustCompile(fmt.Sprintf(`grafana(-enterprise)?[-_](.*)%s`, suffix)).FindSubmatch([]byte(name)) + if len(match) > 0 { + return string(match[2]), nil + } + + return "", errors.New("No version found.") +} diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index 04a9c0ca54c..14e92f01bc0 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -11,7 +11,9 @@ func TestPreparingReleaseFromRemote(t *testing.T) { expectedOs := "linux" buildArtifacts := []buildArtifact{{expectedOs,expectedArch, ".linux-amd64.tar.gz"}} - builder := releaseFromExternalContent{ + var builder releaseBuilder + + builder = releaseFromExternalContent{ getter: mockHttpGetter{}, rawVersion: versionIn, } @@ -49,5 +51,60 @@ func (mockHttpGetter) getContents(url string) (string, error) { func TestPreparingReleaseFromLocal(t *testing.T) { + whatsNewUrl := "https://whatsnews.foo/" + relNotesUrl := "https://relnotes.foo/" + expectedVersion := "5.4.0-123pre1" + expectedBuilds := 4 + var builder releaseBuilder + builder = releaseLocalSources{ + path: "local_test_data", + } + + relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, buildArtifactConfigurations) + + if relAll.Stable || !relAll.Nightly { + t.Error("Expected a nightly release but wasn't.") + } + + if relAll.ReleaseNotesUrl != relNotesUrl { + t.Errorf("expected releaseNotesUrl to be %s, but it was %s", relNotesUrl, relAll.ReleaseNotesUrl) + } + if relAll.WhatsNewUrl != whatsNewUrl { + t.Errorf("expected whatsNewUrl to be %s, but it was %s", whatsNewUrl, relAll.WhatsNewUrl) + } + + if relAll.Beta { + t.Errorf("Expected release to be nightly, not beta.") + } + + if relAll.Version != expectedVersion { + t.Errorf("Expected version=%s, but got=%s", expectedVersion, relAll.Version) + } + + if len(relAll.Builds) != expectedBuilds { + t.Errorf("Expected %v builds, but was %v", expectedBuilds, len(relAll.Builds)) + } + + expectedArch := "amd64" + expectedOs := "win" + relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, []buildArtifact{{ + os: expectedOs, + arch: expectedArch, + urlPostfix: ".windows-amd64.zip", + }}) + + if len(relOne.Builds) != 1 { + t.Errorf("Expected 1 artifact, but was %v", len(relOne.Builds)) + } + + build := relOne.Builds[0] + + if build.Arch != expectedArch { + t.Fatalf("Expected arch to be %s, but was %s", expectedArch, build.Arch) + } + + if build.Os != expectedOs { + t.Fatalf("Expected os to be %s, but was %s", expectedOs, build.Os) + } } From 5da9760aebb19138c69bf7316e359730543b749a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 5 Nov 2018 09:51:54 +0100 Subject: [PATCH 044/116] build: publisher supports both local and remote. --- .../release_publisher/externalrelease.go | 5 +-- .../build/release_publisher/localrelease.go | 23 ++++++------- scripts/build/release_publisher/main.go | 33 ++++++++++++++----- scripts/build/release_publisher/publisher.go | 8 ++--- .../build/release_publisher/publisher_test.go | 25 +++++++++----- 5 files changed, 61 insertions(+), 33 deletions(-) diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go index 795e3bc999b..2d69fa604f8 100644 --- a/scripts/build/release_publisher/externalrelease.go +++ b/scripts/build/release_publisher/externalrelease.go @@ -11,15 +11,16 @@ import ( type releaseFromExternalContent struct { getter urlGetter rawVersion string + artifactConfigurations []buildArtifact } -func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact) (*release, error) { +func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string) (*release, error) { version := re.rawVersion[1:] now := time.Now() isBeta := strings.Contains(version, "beta") builds := []build{} - for _, ba := range artifactConfigurations { + for _, ba := range re.artifactConfigurations { sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, isBeta))) if err != nil { return nil, err diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go index 45bebc524e4..1fb266aa041 100644 --- a/scripts/build/release_publisher/localrelease.go +++ b/scripts/build/release_publisher/localrelease.go @@ -14,14 +14,16 @@ import ( type releaseLocalSources struct { path string + artifactConfigurations []buildArtifact } -func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact) (*release, error) { - buildData := r.findBuilds(artifactConfigurations, baseArchiveUrl) +func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string) (*release, error) { + buildData := r.findBuilds(baseArchiveUrl) + now := time.Now() rel := release{ Version: buildData.version, - ReleaseDate: time.Time{}, + ReleaseDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local), Stable: false, Beta: false, Nightly: true, @@ -38,9 +40,9 @@ type buildData struct { builds []build } -func (r releaseLocalSources) findBuilds(buildArtifacts []buildArtifact, baseArchiveUrl string) buildData { +func (r releaseLocalSources) findBuilds(baseArchiveUrl string) buildData { data := buildData{} - filepath.Walk(r.path, createBuildWalker(r.path, &data, buildArtifacts, baseArchiveUrl)) + filepath.Walk(r.path, createBuildWalker(r.path, &data, r.artifactConfigurations, baseArchiveUrl)) return data } @@ -54,14 +56,13 @@ func createBuildWalker(path string, data *buildData, archiveTypes []buildArtifac return nil } - shaBytes, err := ioutil.ReadFile(path + ".sha256") - if err != nil { - log.Fatalf("Failed to read sha256 file %v", err) - } - - for _, archive := range archiveTypes { if strings.HasSuffix(f.Name(), archive.urlPostfix) { + shaBytes, err := ioutil.ReadFile(path + ".sha256") + if err != nil { + log.Fatalf("Failed to read sha256 file %v", err) + } + version, err := grabVersion(f.Name(), archive.urlPostfix) if err != nil { log.Println(err) diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go index 8b3ec8da74e..66ab38ab00e 100644 --- a/scripts/build/release_publisher/main.go +++ b/scripts/build/release_publisher/main.go @@ -13,6 +13,7 @@ func main() { var releaseNotesUrl string var dryRun bool var enterprise bool + var fromLocal bool var apiKey string flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)") @@ -21,6 +22,7 @@ func main() { flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)") flag.BoolVar(&dryRun, "dry-run", false, "--dry-run") flag.BoolVar(&enterprise, "enterprise", false, "--enterprise") + flag.BoolVar(&fromLocal, "from-local", false, "--from-local") flag.Parse() if len(os.Args) == 1 { @@ -33,24 +35,39 @@ func main() { log.Println("Dry-run has been enabled.") } var baseUrl string + var builder releaseBuilder + var product string + + if fromLocal { + path, _ := os.Getwd() + builder = releaseLocalSources{ + path: path, + artifactConfigurations: buildArtifactConfigurations, + } + } else { + builder = releaseFromExternalContent{ + getter: getHttpContents{}, + rawVersion: version, + artifactConfigurations: buildArtifactConfigurations, + } + } if enterprise { - baseUrl = fmt.Sprintf("https://s3-us-west-2.amazonaws.com/%s", "grafana-enterprise-releases/release/grafana-enterprise") + baseUrl = "https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/release/grafana-enterprise" + product = "grafana-enterprise" } else { - baseUrl = fmt.Sprintf("https://s3-us-west-2.amazonaws.com/%s", "grafana-releases/release/grafana") + baseUrl = "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana" + product = "grafana" } p := publisher{ apiKey: apiKey, - baseUri: "https://grafana.com/api", - product: "grafana", + apiUri: "https://grafana.com/api", + product: product, dryRun: dryRun, enterprise: enterprise, baseArchiveUrl: baseUrl, - builder: releaseFromExternalContent{ - getter: getHttpContents{}, - rawVersion: version, - }, + builder: builder, } if err := p.doRelease(whatsNewUrl, releaseNotesUrl); err != nil { log.Fatalf("error: %v", err) diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index e8c3b676e59..0874c1357b6 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -13,7 +13,7 @@ import ( type publisher struct { apiKey string - baseUri string + apiUri string product string dryRun bool enterprise bool @@ -22,11 +22,11 @@ type publisher struct { } type releaseBuilder interface { - prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact) (*release, error) + prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string) (*release, error) } func (p *publisher) doRelease(whatsNewUrl string, releaseNotesUrl string) error { - currentRelease, err := p.builder.prepareRelease(p.baseArchiveUrl, whatsNewUrl, releaseNotesUrl, buildArtifactConfigurations) + currentRelease, err := p.builder.prepareRelease(p.baseArchiveUrl, whatsNewUrl, releaseNotesUrl) if err != nil { return err } @@ -151,7 +151,7 @@ func newBuild(baseArchiveUrl string, ba buildArtifact, version string, isBeta bo } func (p *publisher) apiUrl(url string) string { - return fmt.Sprintf("%s/%s%s", p.baseUri, p.product, url) + return fmt.Sprintf("%s/%s%s", p.apiUri, p.product, url) } func (p *publisher) postRequest(url string, obj interface{}, desc string) error { diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index 14e92f01bc0..fed17007bb6 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -16,9 +16,10 @@ func TestPreparingReleaseFromRemote(t *testing.T) { builder = releaseFromExternalContent{ getter: mockHttpGetter{}, rawVersion: versionIn, + artifactConfigurations: buildArtifactConfigurations, } - rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl, buildArtifacts) + rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl) if !rel.Beta || rel.Stable { t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn) @@ -57,11 +58,13 @@ func TestPreparingReleaseFromLocal(t *testing.T) { expectedBuilds := 4 var builder releaseBuilder + testDataPath := "local_test_data" builder = releaseLocalSources{ - path: "local_test_data", + path: testDataPath, + artifactConfigurations: buildArtifactConfigurations, } - relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, buildArtifactConfigurations) + relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl) if relAll.Stable || !relAll.Nightly { t.Error("Expected a nightly release but wasn't.") @@ -88,11 +91,17 @@ func TestPreparingReleaseFromLocal(t *testing.T) { expectedArch := "amd64" expectedOs := "win" - relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, []buildArtifact{{ - os: expectedOs, - arch: expectedArch, - urlPostfix: ".windows-amd64.zip", - }}) + + builder = releaseLocalSources{ + path: testDataPath, + artifactConfigurations: []buildArtifact{{ + os: expectedOs, + arch: expectedArch, + urlPostfix: ".windows-amd64.zip", + }}, + } + + relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl) if len(relOne.Builds) != 1 { t.Errorf("Expected 1 artifact, but was %v", len(relOne.Builds)) From d728a3c521683ec5007fc09b5dd7b62e480c6686 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 5 Nov 2018 14:52:23 +0100 Subject: [PATCH 045/116] build: publisher uses local time. Previously the local day was used but the timestamp was set to midnight. --- scripts/build/release_publisher/externalrelease.go | 3 +-- scripts/build/release_publisher/localrelease.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go index 2d69fa604f8..2c9f9e1631a 100644 --- a/scripts/build/release_publisher/externalrelease.go +++ b/scripts/build/release_publisher/externalrelease.go @@ -16,7 +16,6 @@ type releaseFromExternalContent struct { func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string) (*release, error) { version := re.rawVersion[1:] - now := time.Now() isBeta := strings.Contains(version, "beta") builds := []build{} @@ -30,7 +29,7 @@ func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl r := release{ Version: version, - ReleaseDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local), + ReleaseDate: time.Now(), Stable: !isBeta, Beta: isBeta, Nightly: false, diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go index 1fb266aa041..bc2e95f7cd8 100644 --- a/scripts/build/release_publisher/localrelease.go +++ b/scripts/build/release_publisher/localrelease.go @@ -20,10 +20,9 @@ type releaseLocalSources struct { func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string) (*release, error) { buildData := r.findBuilds(baseArchiveUrl) - now := time.Now() rel := release{ Version: buildData.version, - ReleaseDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local), + ReleaseDate: time.Now(), Stable: false, Beta: false, Nightly: true, From 30e924611d18965e875874d8186a51cf4367f123 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 15:42:26 +0100 Subject: [PATCH 046/116] changelog: adds note about closing #13322 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d139009ac78..2a30b44161a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ # 5.3.3 (unreleased) +* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322) * **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769) # 5.3.2 (2018-10-24) From 1de35c43a79d913cbb59cd8531df48bb267f8b61 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 5 Nov 2018 16:26:19 +0100 Subject: [PATCH 047/116] build: publishes grafana enterprise to grafana.com --- .circleci/config.yml | 3 +++ scripts/build/publish.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index af5acef4bea..5907e8d4862 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -335,6 +335,9 @@ jobs: - run: name: deploy to gcp command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master' + - run: + name: Deploy to grafana.com + command: 'cd enterprise-dist && scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local' deploy-enterprise-release: diff --git a/scripts/build/publish.sh b/scripts/build/publish.sh index b3fab180ac9..c03146eb910 100755 --- a/scripts/build/publish.sh +++ b/scripts/build/publish.sh @@ -1,4 +1,4 @@ -#/bin/sh +#!/bin/sh # no relation to publish.go From 272c43f7b73e49d8acaf883cb9b4b4c4ed89c386 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 5 Nov 2018 16:57:30 +0100 Subject: [PATCH 048/116] build: minor publisher fixes. --- .circleci/config.yml | 2 +- scripts/build/release_publisher/externalrelease.go | 2 +- scripts/build/release_publisher/localrelease.go | 2 +- scripts/build/release_publisher/publisher_test.go | 2 +- .../grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz | 0 .../grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 | 0 .../grafana-enterprise-5.4.0-123pre1.windows-amd64.zip | 0 .../grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 | 0 .../grafana-enterprise-5.4.0-123pre1.x86_64.rpm | 0 .../grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 | 0 .../grafana-enterprise_5.4.0-123pre1_amd64.deb | 0 .../grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 | 0 12 files changed, 4 insertions(+), 4 deletions(-) rename scripts/build/release_publisher/{local_test_data => testdata}/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz (100%) rename scripts/build/release_publisher/{local_test_data => testdata}/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 (100%) rename scripts/build/release_publisher/{local_test_data => testdata}/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip (100%) rename scripts/build/release_publisher/{local_test_data => testdata}/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 (100%) rename scripts/build/release_publisher/{local_test_data => testdata}/grafana-enterprise-5.4.0-123pre1.x86_64.rpm (100%) rename scripts/build/release_publisher/{local_test_data => testdata}/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 (100%) rename scripts/build/release_publisher/{local_test_data => testdata}/grafana-enterprise_5.4.0-123pre1_amd64.deb (100%) rename scripts/build/release_publisher/{local_test_data => testdata}/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5907e8d4862..b173dbff481 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -337,7 +337,7 @@ jobs: command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master' - run: name: Deploy to grafana.com - command: 'cd enterprise-dist && scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local' + command: 'cd enterprise-dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local' deploy-enterprise-release: diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go index 2c9f9e1631a..d6e6a669293 100644 --- a/scripts/build/release_publisher/externalrelease.go +++ b/scripts/build/release_publisher/externalrelease.go @@ -29,7 +29,7 @@ func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl r := release{ Version: version, - ReleaseDate: time.Now(), + ReleaseDate: time.Now().UTC(), Stable: !isBeta, Beta: isBeta, Nightly: false, diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go index bc2e95f7cd8..898820f97da 100644 --- a/scripts/build/release_publisher/localrelease.go +++ b/scripts/build/release_publisher/localrelease.go @@ -22,7 +22,7 @@ func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, rel := release{ Version: buildData.version, - ReleaseDate: time.Now(), + ReleaseDate: time.Now().UTC(), Stable: false, Beta: false, Nightly: true, diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index fed17007bb6..4e553362dcb 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -58,7 +58,7 @@ func TestPreparingReleaseFromLocal(t *testing.T) { expectedBuilds := 4 var builder releaseBuilder - testDataPath := "local_test_data" + testDataPath := "testdata" builder = releaseLocalSources{ path: testDataPath, artifactConfigurations: buildArtifactConfigurations, diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz similarity index 100% rename from scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz rename to scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 similarity index 100% rename from scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 rename to scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip similarity index 100% rename from scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip rename to scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 similarity index 100% rename from scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 rename to scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm similarity index 100% rename from scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm rename to scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 similarity index 100% rename from scripts/build/release_publisher/local_test_data/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 rename to scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb b/scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb similarity index 100% rename from scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb rename to scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb diff --git a/scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 similarity index 100% rename from scripts/build/release_publisher/local_test_data/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 rename to scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 From 17adb58d803008562faaaa6ff51546816154e773 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 17:32:28 +0100 Subject: [PATCH 049/116] export: provide more help regarding export format this will provide the user with more info about the export format and default to not use the format for sharing on grafana.com etc. ref #13781 --- .../dashboard/export/export_modal.html | 12 +++++- .../features/dashboard/export/export_modal.ts | 40 ++++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard/export/export_modal.html b/public/app/features/dashboard/export/export_modal.html index 0598c612fd6..3505e50b821 100644 --- a/public/app/features/dashboard/export/export_modal.html +++ b/public/app/features/dashboard/export/export_modal.html @@ -15,11 +15,19 @@ You can share dashboards on
Grafana.com

+ + +
- - Cancel diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index f99946915d6..08a79702ed5 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -8,27 +8,47 @@ export class DashExportCtrl { dash: any; exporter: DashboardExporter; dismiss: () => void; + shareExternally: boolean; /** @ngInject */ constructor(private dashboardSrv, datasourceSrv, private $scope, private $rootScope) { this.exporter = new DashboardExporter(datasourceSrv); - this.exporter.makeExportable(this.dashboardSrv.getCurrent()).then(dash => { - this.$scope.$apply(() => { - this.dash = dash; - }); - }); + this.dash = this.dashboardSrv.getCurrent(); } - save() { - const blob = new Blob([angular.toJson(this.dash, true)], { + saveDashboardAsFile() { + if (this.shareExternally) { + this.exporter.makeExportable(this.dash).then((dashboardJson: any) => { + this.$scope.$apply(() => { + this._saveFile(dashboardJson); + }); + }); + } else { + this._saveFile(this.dash.getSaveModelClone()); + } + } + + viewJson() { + if (this.shareExternally) { + this.exporter.makeExportable(this.dash).then((dashboardJson: any) => { + this.$scope.$apply(() => { + this._viewJson(dashboardJson); + }); + }); + } else { + this._viewJson(this.dash.getSaveModelClone()); + } + } + + _saveFile(dash: any) { + const blob = new Blob([angular.toJson(dash, true)], { type: 'application/json;charset=utf-8', }); - saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json'); + saveAs(blob, dash.title + '-' + new Date().getTime() + '.json'); } - saveJson() { - const clone = this.dash; + _viewJson(clone: any) { const editScope = this.$rootScope.$new(); editScope.object = clone; editScope.enableCopy = true; From 35e62bbbe0eebfc9b2cc025800c4f3982c55e2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Nov 2018 17:46:09 +0100 Subject: [PATCH 050/116] wip: react panel options architecture --- .../dashboard/dashgrid/PanelChrome.tsx | 25 +++++- .../dashboard/dashgrid/PanelEditor.tsx | 28 ++++--- public/app/features/dashboard/panel_model.ts | 19 ++++- .../features/dashboard/settings/settings.ts | 2 +- public/app/plugins/panel/graph2/module.tsx | 35 +++++--- public/app/types/panel.ts | 9 +- public/app/types/plugins.ts | 11 ++- public/app/viz/Graph.tsx | 84 +++++++++++-------- 8 files changed, 148 insertions(+), 65 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 82b366d8126..953dfd62368 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -21,6 +21,7 @@ export interface Props { export interface State { refreshCounter: number; + renderCounter: number; timeRange?: TimeRange; } @@ -30,11 +31,13 @@ export class PanelChrome extends PureComponent { this.state = { refreshCounter: 0, + renderCounter: 0, }; } componentDidMount() { this.props.panel.events.on('refresh', this.onRefresh); + this.props.panel.events.on('render', this.onRender); this.props.dashboard.panelInitialized(this.props.panel); } @@ -52,6 +55,13 @@ export class PanelChrome extends PureComponent { }); }; + onRender = () => { + console.log('onRender'); + this.setState({ + renderCounter: this.state.renderCounter + 1, + }); + }; + get isVisible() { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } @@ -59,9 +69,11 @@ export class PanelChrome extends PureComponent { render() { const { panel, dashboard } = this.props; const { datasource, targets } = panel; - const { refreshCounter, timeRange } = this.state; + const { timeRange, renderCounter, refreshCounter } = this.state; const PanelComponent = this.props.component; + console.log('Panel chrome render'); + return (
@@ -74,7 +86,16 @@ export class PanelChrome extends PureComponent { refreshCounter={refreshCounter} > {({ loading, timeSeries }) => { - return ; + console.log('panelcrome inner render'); + return ( + + ); }}
diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 26ac8b7d2c1..9d98621f9b6 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -1,12 +1,15 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import classNames from 'classnames'; + +import { QueriesTab } from './QueriesTab'; +import { VizTypePicker } from './VizTypePicker'; + +import { store } from 'app/store/configureStore'; +import { updateLocation } from 'app/core/actions'; + import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { store } from 'app/store/configureStore'; -import { QueriesTab } from './QueriesTab'; import { PanelPlugin, PluginExports } from 'app/types/plugins'; -import { VizTypePicker } from './VizTypePicker'; -import { updateLocation } from 'app/core/actions'; interface PanelEditorProps { panel: PanelModel; @@ -22,7 +25,7 @@ interface PanelEditorTab { icon: string; } -export class PanelEditor extends React.Component { +export class PanelEditor extends PureComponent { tabs: PanelEditorTab[]; constructor(props) { @@ -39,16 +42,20 @@ export class PanelEditor extends React.Component { } renderPanelOptions() { - const { pluginExports } = this.props; + const { pluginExports, panel } = this.props; - if (pluginExports.PanelOptions) { - const PanelOptions = pluginExports.PanelOptions; - return ; + if (pluginExports.PanelOptionsComponent) { + const OptionsComponent = pluginExports.PanelOptionsComponent; + return ; } else { return

Visualization has no options

; } } + onPanelOptionsChanged = (options: any) => { + this.props.panel.updateOptions(options); + }; + renderVizTab() { return (
@@ -70,6 +77,7 @@ export class PanelEditor extends React.Component { partial: true, }) ); + this.forceUpdate(); }; render() { diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index d82368d8dd7..ed032a118fe 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -60,6 +60,21 @@ export class PanelModel { _.defaultsDeep(this, _.cloneDeep(defaults)); } + getOptions() { + return this[this.getOptionsKey()] || {}; + } + + updateOptions(options: object) { + const update: any = {}; + update[this.getOptionsKey()] = options; + Object.assign(this, update); + this.render(); + } + + private getOptionsKey() { + return this.type + 'Options'; + } + getSaveModel() { const model: any = {}; for (const property in this) { @@ -121,10 +136,6 @@ export class PanelModel { this.events.emit('panel-initialized'); } - initEditMode() { - this.events.emit('panel-init-edit-mode'); - } - changeType(pluginId: string) { this.type = pluginId; diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index b6a70ee4b98..1e8d96a54cb 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -32,9 +32,9 @@ export class SettingsCtrl { this.$scope.$on('$destroy', () => { this.dashboard.updateSubmenuVisibility(); - this.dashboard.startRefresh(); setTimeout(() => { this.$rootScope.appEvent('dash-scroll', { restore: true }); + this.dashboard.startRefresh(); }); }); diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 7bb2ab48e40..68068268dd4 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,13 +1,10 @@ -// Libraries import _ from 'lodash'; import React, { PureComponent } from 'react'; -// Components import Graph from 'app/viz/Graph'; -import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; import { Switch } from 'app/core/components/Switch/Switch'; -// Types +import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; import { PanelProps, NullValueMode } from 'app/types'; interface Options { @@ -18,9 +15,7 @@ interface Options { onChange: (options: Options) => void; } -interface Props extends PanelProps { - options: Options; -} +interface Props extends PanelProps {} export class Graph2 extends PureComponent { constructor(props) { @@ -29,17 +24,26 @@ export class Graph2 extends PureComponent { render() { const { timeSeries, timeRange } = this.props; + const { showLines, showBars, showPoints } = this.props.options; const vmSeries = getTimeSeriesVMs({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, }); - return ; + return ( + + ); } } -export class TextOptions extends PureComponent { +export class GraphOptions extends PureComponent { onToggleLines = () => { const options = this.props as Options; @@ -49,6 +53,15 @@ export class TextOptions extends PureComponent { }); }; + onTogglePoints = () => { + const options = this.props as Options; + + this.props.onChange({ + ...options, + showPoints: !this.props.showPoints, + }); + }; + render() { const { showBars, showPoints, showLines } = this.props; @@ -58,11 +71,11 @@ export class TextOptions extends PureComponent {
Draw Modes
- +
); } } -export { Graph2 as PanelComponent, TextOptions as PanelOptions }; +export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent }; diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index 5ece77fc5aa..5207c17ada9 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -1,7 +1,14 @@ import { LoadingState, TimeSeries, TimeRange } from './series'; -export interface PanelProps { +export interface PanelProps { timeSeries: TimeSeries[]; timeRange: TimeRange; loading: LoadingState; + options: T; + renderCounter: number; +} + +export interface PanelOptionProps { + options: T; + onChange: (options: T) => void; } diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 1b5499f88b7..4b172c0eef4 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -1,13 +1,18 @@ +import { ComponentClass } from 'react'; +import { PanelProps, PanelOptionProps } from './panel'; + export interface PluginExports { - PanelCtrl?; - PanelComponent?: any; Datasource?: any; QueryCtrl?: any; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; - PanelOptions?: any; ExploreQueryField?: any; ExploreStartPage?: any; + + // Panel plugin + PanelCtrl?; + PanelComponent?: ComponentClass; + PanelOptionsComponent: ComponentClass; } export interface PanelPlugin { diff --git a/public/app/viz/Graph.tsx b/public/app/viz/Graph.tsx index fab65225715..5d99f4e0c7f 100644 --- a/public/app/viz/Graph.tsx +++ b/public/app/viz/Graph.tsx @@ -34,37 +34,22 @@ function time_format(ticks, min, max) { return '%H:%M'; } -const FLOT_OPTIONS = { - legend: { - show: false, - }, - series: { - lines: { - linewidth: 1, - zero: false, - }, - shadowSize: 0, - }, - grid: { - minBorderMargin: 0, - markings: [], - backgroundColor: null, - borderWidth: 0, - // hoverable: true, - clickable: true, - color: '#a1a1a1', - margin: { left: 0, right: 0 }, - labelMarginX: 0, - }, -}; - interface GraphProps { timeSeries: TimeSeriesVMs; timeRange: TimeRange; + showLines?: boolean; + showPoints?: boolean; + showBars?: boolean; size?: { width: number; height: number }; } export class Graph extends PureComponent { + static defaultProps = { + showLines: true, + showPoints: false, + showBars: false, + }; + element: any; componentDidUpdate(prevProps: GraphProps) { @@ -82,7 +67,7 @@ export class Graph extends PureComponent { } draw() { - const { size, timeSeries, timeRange } = this.props; + const { size, timeSeries, timeRange, showLines, showBars, showPoints } = this.props; if (!size) { return; @@ -92,7 +77,31 @@ export class Graph extends PureComponent { const min = timeRange.from.valueOf(); const max = timeRange.to.valueOf(); - const dynamicOptions = { + const flotOptions = { + legend: { + show: false, + }, + series: { + lines: { + show: showLines, + linewidth: 1, + zero: false, + }, + points: { + show: showPoints, + fill: 1, + fillColor: false, + radius: 2, + }, + bars: { + show: showBars, + fill: 1, + barWidth: 1, + zero: false, + lineWidth: 0, + }, + shadowSize: 0, + }, xaxis: { mode: 'time', min: min, @@ -101,15 +110,24 @@ export class Graph extends PureComponent { ticks: ticks, timeformat: time_format(ticks, min, max), }, + grid: { + minBorderMargin: 0, + markings: [], + backgroundColor: null, + borderWidth: 0, + // hoverable: true, + clickable: true, + color: '#a1a1a1', + margin: { left: 0, right: 0 }, + labelMarginX: 0, + }, }; - const options = { - ...FLOT_OPTIONS, - ...dynamicOptions, - }; - - console.log('plot', timeSeries, options); - $.plot(this.element, timeSeries, options); + try { + $.plot(this.element, timeSeries, flotOptions); + } catch (err) { + console.log('Graph rendering error', err, flotOptions, timeSeries); + } } render() { From 9cce0f553a554d037b581d132cad69f1d07d5818 Mon Sep 17 00:00:00 2001 From: Zac Coffy Date: Mon, 5 Nov 2018 13:15:03 -0500 Subject: [PATCH 051/116] Adding Cloudwatch AWS/Connect metrics and dimensions --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 718f9e0d253..1a860519f2b 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -46,6 +46,7 @@ func init() { "AWS/Billing": {"EstimatedCharges"}, "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, "AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"}, + "AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"}, "AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"}, "AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"}, "AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"}, @@ -120,6 +121,7 @@ func init() { "AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"}, "AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudSearch": {}, + "AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"}, "AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"}, "AWS/DX": {"ConnectionId"}, "AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"}, From 9393b06166564bd80d483aff48fef145d35e6e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Nov 2018 10:31:39 -0800 Subject: [PATCH 052/116] basic panel options working --- .../dashboard/dashgrid/PanelEditor.tsx | 1 + public/app/plugins/panel/graph2/module.tsx | 27 ++++++++++--------- public/app/types/index.ts | 3 ++- public/app/types/panel.ts | 2 +- public/app/types/plugins.ts | 4 +-- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 9d98621f9b6..371ed22fff7 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -54,6 +54,7 @@ export class PanelEditor extends PureComponent { onPanelOptionsChanged = (options: any) => { this.props.panel.updateOptions(options); + this.forceUpdate(); }; renderVizTab() { diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 68068268dd4..a666d762062 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -5,7 +5,7 @@ import Graph from 'app/viz/Graph'; import { Switch } from 'app/core/components/Switch/Switch'; import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; -import { PanelProps, NullValueMode } from 'app/types'; +import { PanelProps, PanelOptionsProps, NullValueMode } from 'app/types'; interface Options { showBars: boolean; @@ -43,34 +43,37 @@ export class Graph2 extends PureComponent { } } -export class GraphOptions extends PureComponent { +export class GraphOptions extends PureComponent> { onToggleLines = () => { - const options = this.props as Options; - this.props.onChange({ - ...options, - showLines: !this.props.showLines, + ...this.props.options, + showLines: !this.props.options.showLines, + }); + }; + + onToggleBars = () => { + this.props.onChange({ + ...this.props.options, + showBars: !this.props.options.showBars, }); }; onTogglePoints = () => { - const options = this.props as Options; - this.props.onChange({ - ...options, - showPoints: !this.props.showPoints, + ...this.props.options, + showPoints: !this.props.options.showPoints, }); }; render() { - const { showBars, showPoints, showLines } = this.props; + const { showBars, showPoints, showLines } = this.props.options; return (
Draw Modes
- +
diff --git a/public/app/types/index.ts b/public/app/types/index.ts index c51622682d4..fc176fed7e2 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -20,7 +20,7 @@ import { DataQueryResponse, DataQueryOptions, } from './series'; -import { PanelProps } from './panel'; +import { PanelProps, PanelOptionsProps } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; import { Organization, OrganizationPreferences, OrganizationState } from './organization'; import { @@ -69,6 +69,7 @@ export { TimeRange, LoadingState, PanelProps, + PanelOptionsProps, TimeSeries, TimeSeriesVM, TimeSeriesVMs, diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index 5207c17ada9..7febd0cad26 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -8,7 +8,7 @@ export interface PanelProps { renderCounter: number; } -export interface PanelOptionProps { +export interface PanelOptionsProps { options: T; onChange: (options: T) => void; } diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 4b172c0eef4..817777669d8 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -1,5 +1,5 @@ import { ComponentClass } from 'react'; -import { PanelProps, PanelOptionProps } from './panel'; +import { PanelProps, PanelOptionsProps } from './panel'; export interface PluginExports { Datasource?: any; @@ -12,7 +12,7 @@ export interface PluginExports { // Panel plugin PanelCtrl?; PanelComponent?: ComponentClass; - PanelOptionsComponent: ComponentClass; + PanelOptionsComponent: ComponentClass; } export interface PanelPlugin { From b9612aaa231b54cf2973b486fb35916a38e9e0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Nov 2018 10:38:55 -0800 Subject: [PATCH 053/116] minor code style change --- public/app/plugins/panel/graph2/module.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index a666d762062..837109f6be7 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -45,24 +45,15 @@ export class Graph2 extends PureComponent { export class GraphOptions extends PureComponent> { onToggleLines = () => { - this.props.onChange({ - ...this.props.options, - showLines: !this.props.options.showLines, - }); + this.props.onChange({ showLines: !this.props.options.showLines, ...this.props.options }); }; onToggleBars = () => { - this.props.onChange({ - ...this.props.options, - showBars: !this.props.options.showBars, - }); + this.props.onChange({ showBars: !this.props.options.showBars, ...this.props.options }); }; onTogglePoints = () => { - this.props.onChange({ - ...this.props.options, - showPoints: !this.props.options.showPoints, - }); + this.props.onChange({ showPoints: !this.props.options.showPoints, ...this.props.options }); }; render() { From c1ca1ed35e1441e989787816dd7898cedff60a9d Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 5 Nov 2018 23:36:58 +0100 Subject: [PATCH 054/116] Time selection via graph --- public/app/features/explore/Explore.tsx | 9 ++++++- public/app/features/explore/Graph.tsx | 28 ++++++++++++++++++---- public/app/features/explore/Logs.tsx | 2 ++ public/app/features/explore/TimePicker.tsx | 28 +++++++++++++++++----- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 6ec6c79ac5f..78764ab9876 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -898,6 +898,7 @@ export class Explore extends React.PureComponent { height={graphHeight} loading={graphLoading} id={`explore-graph-${position}`} + onChangeTime={this.onChangeTime} range={graphRange} split={split} /> @@ -908,7 +909,13 @@ export class Explore extends React.PureComponent {
) : null} {supportsLogs && showingLogs ? ( - + ) : null} )} diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 6c22ca67509..2c1f08b871d 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -5,6 +5,7 @@ import { withSize } from 'react-sizeme'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.time'; +import 'vendor/flot/jquery.flot.selection'; import { RawTimeRange } from 'app/types/series'; import * as dateMath from 'app/core/utils/datemath'; @@ -62,10 +63,10 @@ const FLOT_OPTIONS = { margin: { left: 0, right: 0 }, labelMarginX: 0, }, - // selection: { - // mode: 'x', - // color: '#666', - // }, + selection: { + mode: 'x', + color: '#666', + }, // crosshair: { // mode: 'x', // }, @@ -80,6 +81,7 @@ interface GraphProps { split?: boolean; size?: { width: number; height: number }; userOptions?: any; + onChangeTime?: (range: RawTimeRange) => void; } interface GraphState { @@ -87,6 +89,8 @@ interface GraphState { } export class Graph extends PureComponent { + $el: any; + state = { showAllTimeSeries: false, }; @@ -99,6 +103,8 @@ export class Graph extends PureComponent { componentDidMount() { this.draw(); + this.$el = $(`#${this.props.id}`); + this.$el.bind('plotselected', this.onPlotSelected); } componentDidUpdate(prevProps: GraphProps) { @@ -113,6 +119,20 @@ export class Graph extends PureComponent { } } + componentWillUnmount() { + this.$el.unbind('plotselected', this.onPlotSelected); + } + + onPlotSelected = (event, ranges) => { + if (this.props.onChangeTime) { + const range = { + from: moment(ranges.xaxis.from), + to: moment(ranges.xaxis.to), + }; + this.props.onChangeTime(range); + } + }; + onShowAllTimeSeries = () => { this.setState( { diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 5630d1de8f6..ccfa96bed0b 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -25,6 +25,7 @@ interface LogsProps { loading: boolean; position: string; range?: RawTimeRange; + onChangeTime?: (range: RawTimeRange) => void; } interface LogsState { @@ -88,6 +89,7 @@ export default class Logs extends PureComponent { height="100px" range={range} id={`explore-logs-graph-${position}`} + onChangeTime={this.props.onChangeTime} userOptions={graphOptions} />
diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx index 8955fb4aa9b..ed2fd924c78 100644 --- a/public/app/features/explore/TimePicker.tsx +++ b/public/app/features/explore/TimePicker.tsx @@ -16,6 +16,9 @@ export const DEFAULT_RANGE = { * @param value Epoch or relative time */ export function parseTime(value: string, isUtc = false): string { + if (moment.isMoment(value)) { + return value; + } if (value.indexOf('now') !== -1) { return value; } @@ -39,7 +42,8 @@ interface TimePickerState { isOpen: boolean; isUtc: boolean; rangeString: string; - refreshInterval: string; + refreshInterval?: string; + initialRange: RawTimeRange; // Input-controlled text, keep these in a shape that is human-editable fromRaw: string; @@ -49,11 +53,24 @@ interface TimePickerState { export default class TimePicker extends PureComponent { dropdownEl: any; - constructor(props) { - super(props); + state = { + isOpen: false, + isUtc: false, + rangeString: '', + initialRange: DEFAULT_RANGE, + fromRaw: '', + toRaw: '', + refreshInterval: '', + }; + + static getDerivedStateFromProps(props, state) { + if (state.range && state.range === props.range) { + return null; + } const from = props.range ? props.range.from : DEFAULT_RANGE.from; const to = props.range ? props.range.to : DEFAULT_RANGE.to; + const initialRange = props.range || DEFAULT_RANGE; // Ensure internal format const fromRaw = parseTime(from, props.isUtc); @@ -63,13 +80,12 @@ export default class TimePicker extends PureComponent Date: Tue, 6 Nov 2018 07:23:02 +0100 Subject: [PATCH 055/116] fixed options --- public/app/plugins/panel/graph2/module.tsx | 6 +-- public/app/viz/Graph.tsx | 52 +++++++++++----------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 837109f6be7..b132d3374f1 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -45,15 +45,15 @@ export class Graph2 extends PureComponent { export class GraphOptions extends PureComponent> { onToggleLines = () => { - this.props.onChange({ showLines: !this.props.options.showLines, ...this.props.options }); + this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines }); }; onToggleBars = () => { - this.props.onChange({ showBars: !this.props.options.showBars, ...this.props.options }); + this.props.onChange({ ...this.props.options, showBars: !this.props.options.showBars }); }; onTogglePoints = () => { - this.props.onChange({ showPoints: !this.props.options.showPoints, ...this.props.options }); + this.props.onChange({ ...this.props.options, showPoints: !this.props.options.showPoints }); }; render() { diff --git a/public/app/viz/Graph.tsx b/public/app/viz/Graph.tsx index 5d99f4e0c7f..566080fbc92 100644 --- a/public/app/viz/Graph.tsx +++ b/public/app/viz/Graph.tsx @@ -8,32 +8,6 @@ import 'vendor/flot/jquery.flot.time'; // Types import { TimeRange, TimeSeriesVMs } from 'app/types'; -// Copied from graph.ts -function time_format(ticks, min, max) { - if (min && max && ticks) { - const range = max - min; - const secPerTick = range / ticks / 1000; - const oneDay = 86400000; - const oneYear = 31536000000; - - if (secPerTick <= 45) { - return '%H:%M:%S'; - } - if (secPerTick <= 7200 || range <= oneDay) { - return '%H:%M'; - } - if (secPerTick <= 80000) { - return '%m/%d %H:%M'; - } - if (secPerTick <= 2419200 || range <= oneYear) { - return '%m/%d'; - } - return '%Y-%m'; - } - - return '%H:%M'; -} - interface GraphProps { timeSeries: TimeSeriesVMs; timeRange: TimeRange; @@ -139,4 +113,30 @@ export class Graph extends PureComponent { } } +// Copied from graph.ts +function time_format(ticks, min, max) { + if (min && max && ticks) { + const range = max - min; + const secPerTick = range / ticks / 1000; + const oneDay = 86400000; + const oneYear = 31536000000; + + if (secPerTick <= 45) { + return '%H:%M:%S'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return '%H:%M'; + } + if (secPerTick <= 80000) { + return '%m/%d %H:%M'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return '%m/%d'; + } + return '%Y-%m'; + } + + return '%H:%M'; +} + export default withSize()(Graph); From 7bde98aff9789c071bb3221d50ffe17798e371bb Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 6 Nov 2018 09:00:17 +0100 Subject: [PATCH 056/116] rename and mark functions as private --- public/app/features/dashboard/export/export_modal.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 08a79702ed5..0e48041ca87 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -21,11 +21,11 @@ export class DashExportCtrl { if (this.shareExternally) { this.exporter.makeExportable(this.dash).then((dashboardJson: any) => { this.$scope.$apply(() => { - this._saveFile(dashboardJson); + this.openSaveAsDialog(dashboardJson); }); }); } else { - this._saveFile(this.dash.getSaveModelClone()); + this.openSaveAsDialog(this.dash.getSaveModelClone()); } } @@ -33,22 +33,22 @@ export class DashExportCtrl { if (this.shareExternally) { this.exporter.makeExportable(this.dash).then((dashboardJson: any) => { this.$scope.$apply(() => { - this._viewJson(dashboardJson); + this.openJsonModal(dashboardJson); }); }); } else { - this._viewJson(this.dash.getSaveModelClone()); + this.openJsonModal(this.dash.getSaveModelClone()); } } - _saveFile(dash: any) { + private openSaveAsDialog(dash: any) { const blob = new Blob([angular.toJson(dash, true)], { type: 'application/json;charset=utf-8', }); saveAs(blob, dash.title + '-' + new Date().getTime() + '.json'); } - _viewJson(clone: any) { + private openJsonModal(clone: any) { const editScope = this.$rootScope.$new(); editScope.object = clone; editScope.enableCopy = true; From c9a4da42706ed634f6f2b487905b87a5b2d1c59e Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 6 Nov 2018 09:40:41 +0100 Subject: [PATCH 057/116] build: publisher handles nightly builds. --- .../release_publisher/externalrelease.go | 6 ++--- .../build/release_publisher/localrelease.go | 4 +-- scripts/build/release_publisher/main.go | 25 +++++++++++++++---- scripts/build/release_publisher/publisher.go | 6 ++--- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go index d6e6a669293..327fb335dbe 100644 --- a/scripts/build/release_publisher/externalrelease.go +++ b/scripts/build/release_publisher/externalrelease.go @@ -14,7 +14,7 @@ type releaseFromExternalContent struct { artifactConfigurations []buildArtifact } -func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string) (*release, error) { +func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) { version := re.rawVersion[1:] isBeta := strings.Contains(version, "beta") @@ -30,9 +30,9 @@ func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl r := release{ Version: version, ReleaseDate: time.Now().UTC(), - Stable: !isBeta, + Stable: !isBeta && !nightly, Beta: isBeta, - Nightly: false, + Nightly: nightly, WhatsNewUrl: whatsNewUrl, ReleaseNotesUrl: releaseNotesUrl, Builds: builds, diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go index 898820f97da..7c0a3b1d085 100644 --- a/scripts/build/release_publisher/localrelease.go +++ b/scripts/build/release_publisher/localrelease.go @@ -17,7 +17,7 @@ type releaseLocalSources struct { artifactConfigurations []buildArtifact } -func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string) (*release, error) { +func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) { buildData := r.findBuilds(baseArchiveUrl) rel := release{ @@ -25,7 +25,7 @@ func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, ReleaseDate: time.Now().UTC(), Stable: false, Beta: false, - Nightly: true, + Nightly: nightly, WhatsNewUrl: whatsNewUrl, ReleaseNotesUrl: releaseNotesUrl, Builds: buildData.builds, diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go index 66ab38ab00e..c4004fe7b4c 100644 --- a/scripts/build/release_publisher/main.go +++ b/scripts/build/release_publisher/main.go @@ -14,6 +14,7 @@ func main() { var dryRun bool var enterprise bool var fromLocal bool + var nightly bool var apiKey string flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)") @@ -22,11 +23,13 @@ func main() { flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)") flag.BoolVar(&dryRun, "dry-run", false, "--dry-run") flag.BoolVar(&enterprise, "enterprise", false, "--enterprise") - flag.BoolVar(&fromLocal, "from-local", false, "--from-local") + flag.BoolVar(&fromLocal, "from-local", false, "--from-local (builds will be tagged as nightly)") flag.Parse() + nightly = fromLocal + if len(os.Args) == 1 { - fmt.Println("Usage: go run publisher.go main.go --version --wn --rn --apikey --dry-run false --enterprise false") + fmt.Println("Usage: go run publisher.go main.go --version --wn --rn --apikey --dry-run false --enterprise false --nightly false") fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run --enterprise") os.Exit(1) } @@ -52,12 +55,14 @@ func main() { } } + archiveProviderRoot := "https://s3-us-west-2.amazonaws.com" + if enterprise { - baseUrl = "https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/release/grafana-enterprise" product = "grafana-enterprise" + baseUrl = createBaseUrl(archiveProviderRoot, "grafana-enterprise-releases", product, nightly) } else { - baseUrl = "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana" product = "grafana" + baseUrl = createBaseUrl(archiveProviderRoot, "grafana-releases", product, nightly) } p := publisher{ @@ -69,7 +74,17 @@ func main() { baseArchiveUrl: baseUrl, builder: builder, } - if err := p.doRelease(whatsNewUrl, releaseNotesUrl); err != nil { + if err := p.doRelease(whatsNewUrl, releaseNotesUrl, nightly); err != nil { log.Fatalf("error: %v", err) } } +func createBaseUrl(root string, bucketName string, product string, nightly bool) string { + var subPath string + if nightly { + subPath = "master" + } else { + subPath = "release" + } + + return fmt.Sprintf("%s/%s/%s/%s", root, bucketName, subPath, product) +} diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index 0874c1357b6..d2c10d1640f 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -22,11 +22,11 @@ type publisher struct { } type releaseBuilder interface { - prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string) (*release, error) + prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) } -func (p *publisher) doRelease(whatsNewUrl string, releaseNotesUrl string) error { - currentRelease, err := p.builder.prepareRelease(p.baseArchiveUrl, whatsNewUrl, releaseNotesUrl) +func (p *publisher) doRelease(whatsNewUrl string, releaseNotesUrl string, nightly bool) error { + currentRelease, err := p.builder.prepareRelease(p.baseArchiveUrl, whatsNewUrl, releaseNotesUrl, nightly) if err != nil { return err } From 34f531e113c67891d0558e23761051edbec75ebf Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 6 Nov 2018 09:52:41 +0100 Subject: [PATCH 058/116] build: fixes --- scripts/build/release_publisher/publisher_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index 4e553362dcb..ee491ce0b98 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -19,7 +19,7 @@ func TestPreparingReleaseFromRemote(t *testing.T) { artifactConfigurations: buildArtifactConfigurations, } - rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl) + rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl, false) if !rel.Beta || rel.Stable { t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn) @@ -64,7 +64,7 @@ func TestPreparingReleaseFromLocal(t *testing.T) { artifactConfigurations: buildArtifactConfigurations, } - relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl) + relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true) if relAll.Stable || !relAll.Nightly { t.Error("Expected a nightly release but wasn't.") @@ -101,7 +101,7 @@ func TestPreparingReleaseFromLocal(t *testing.T) { }}, } - relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl) + relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true) if len(relOne.Builds) != 1 { t.Errorf("Expected 1 artifact, but was %v", len(relOne.Builds)) From a3196a130e2c3a90db0148e39b141f4275d84ba6 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 6 Nov 2018 09:53:02 +0100 Subject: [PATCH 059/116] changelog: add notes about closing #13970 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a30b44161a..fb947317f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Minor * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy) * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm) * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) From e39e82949dccbc5be080a15e01b793385177295a Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 6 Nov 2018 11:07:12 +0100 Subject: [PATCH 060/116] Adaptive bar widths for log graph --- public/app/features/explore/Explore.tsx | 3 +- public/app/features/explore/Graph.tsx | 1 + public/app/features/explore/Logs.tsx | 3 ++ public/app/features/explore/TimePicker.tsx | 33 ++++++++++--------- .../plugins/datasource/logging/datasource.ts | 2 +- .../datasource/logging/result_transformer.ts | 4 +-- 6 files changed, 27 insertions(+), 19 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 78764ab9876..896a946a85b 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -475,7 +475,7 @@ export class Explore extends React.PureComponent { from: parseDate(range.from, false), to: parseDate(range.to, true), }; - const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval); + const { interval, intervalMs } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval); const targets = [ { ...targetOptions, @@ -490,6 +490,7 @@ export class Explore extends React.PureComponent { return { interval, + intervalMs, targets, range: queryRange, }; diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 2c1f08b871d..9e4fea0d3de 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -6,6 +6,7 @@ import { withSize } from 'react-sizeme'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.time'; import 'vendor/flot/jquery.flot.selection'; +import 'vendor/flot/jquery.flot.stack'; import { RawTimeRange } from 'app/types/series'; import * as dateMath from 'app/core/utils/datemath'; diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index ccfa96bed0b..edde5acba92 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -12,7 +12,10 @@ const graphOptions = { series: { bars: { show: true, + lineWidth: 5, + // barWidth: 10, }, + // stack: true, }, yaxis: { tickDecimals: 0, diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx index ed2fd924c78..a3578263cea 100644 --- a/public/app/features/explore/TimePicker.tsx +++ b/public/app/features/explore/TimePicker.tsx @@ -43,7 +43,7 @@ interface TimePickerState { isUtc: boolean; rangeString: string; refreshInterval?: string; - initialRange: RawTimeRange; + initialRange?: RawTimeRange; // Input-controlled text, keep these in a shape that is human-editable fromRaw: string; @@ -53,24 +53,27 @@ interface TimePickerState { export default class TimePicker extends PureComponent { dropdownEl: any; - state = { - isOpen: false, - isUtc: false, - rangeString: '', - initialRange: DEFAULT_RANGE, - fromRaw: '', - toRaw: '', - refreshInterval: '', - }; + constructor(props) { + super(props); + + this.state = { + isOpen: props.isOpen, + isUtc: props.isUtc, + rangeString: '', + fromRaw: '', + toRaw: '', + initialRange: DEFAULT_RANGE, + refreshInterval: '', + }; + } static getDerivedStateFromProps(props, state) { - if (state.range && state.range === props.range) { - return null; + if (state.initialRange && state.initialRange === props.range) { + return state; } const from = props.range ? props.range.from : DEFAULT_RANGE.from; const to = props.range ? props.range.to : DEFAULT_RANGE.to; - const initialRange = props.range || DEFAULT_RANGE; // Ensure internal format const fromRaw = parseTime(from, props.isUtc); @@ -81,10 +84,10 @@ export default class TimePicker extends PureComponent processStream(stream, DEFAULT_LIMIT)); + const processedStreams = allStreams.map(stream => processStream(stream, DEFAULT_LIMIT, options.intervalMs)); return { data: processedStreams }; }); } diff --git a/public/app/plugins/datasource/logging/result_transformer.ts b/public/app/plugins/datasource/logging/result_transformer.ts index 8aa7ebc12e0..e1e622aeb59 100644 --- a/public/app/plugins/datasource/logging/result_transformer.ts +++ b/public/app/plugins/datasource/logging/result_transformer.ts @@ -143,7 +143,7 @@ export function mergeStreams(streams: LogsStream[], limit?: number): LogsModel { return { meta, series, rows: sortedEntries }; } -export function processStream(stream: LogsStream, limit?: number): LogsStream { +export function processStream(stream: LogsStream, limit?: number, intervalMs?: number): LogsStream { const sortedEntries: any[] = _.chain(stream.entries) .map(entry => processEntry(entry, stream)) .sortBy('timestamp') @@ -155,7 +155,7 @@ export function processStream(stream: LogsStream, limit?: number): LogsStream { let previousTime; const datapoints = sortedEntries.reduce((acc, entry, index) => { // Bucket to nearest minute - const time = Math.round(entry.timeJs / 1000 / 60) * 1000 * 60; + const time = Math.round(entry.timeJs / intervalMs / 10) * intervalMs * 10; // Entry for time if (time === previousTime) { acc[acc.length - 1][0]++; From e5e886ccb7bacd483f2423cc9cdc2dea0891e7f0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 6 Nov 2018 11:49:22 +0100 Subject: [PATCH 061/116] fix selecting datasource using enter key --- public/app/core/components/form_dropdown/form_dropdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 6e863e1cb5d..81d4b336443 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -88,7 +88,7 @@ export class FormDropdownCtrl { if (evt.keyCode === 13) { setTimeout(() => { this.inputElement.blur(); - }, 100); + }, 300); } }); From a5ed86edba52db8500234e9f8de2a62669473ebf Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 6 Nov 2018 12:00:05 +0100 Subject: [PATCH 062/116] Graph log entries by log level --- public/app/core/logs_model.ts | 15 ++++- public/app/core/utils/colors.ts | 20 +++--- .../datasource/logging/result_transformer.ts | 66 +++++++++++-------- 3 files changed, 62 insertions(+), 39 deletions(-) diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index ca7899db7d8..42c2e1ae453 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import { TimeSeries } from 'app/core/core'; +import colors from 'app/core/utils/colors'; export enum LogLevel { crit = 'crit', @@ -9,8 +10,20 @@ export enum LogLevel { info = 'info', debug = 'debug', trace = 'trace', + none = 'none', } +export const LogLevelColor = { + [LogLevel.crit]: colors[7], + [LogLevel.warn]: colors[1], + [LogLevel.err]: colors[4], + [LogLevel.error]: colors[4], + [LogLevel.info]: colors[0], + [LogLevel.debug]: colors[3], + [LogLevel.trace]: colors[3], + [LogLevel.none]: '#eee', +}; + export interface LogSearchMatch { start: number; length: number; @@ -44,7 +57,7 @@ export interface LogsStream { labels: string; entries: LogsStreamEntry[]; parsedLabels: { [key: string]: string }; - graphSeries: TimeSeries; + intervalMs?: number; } export interface LogsStreamEntry { diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index e8a7366beb5..16214679996 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -10,16 +10,16 @@ export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; export const REGION_FILL_ALPHA = 0.09; const colors = [ - '#7EB26D', - '#EAB839', - '#6ED0E0', - '#EF843C', - '#E24D42', - '#1F78C1', - '#BA43A9', - '#705DA0', - '#508642', - '#CCA300', + '#7EB26D', // 0: pale green + '#EAB839', // 1: mustard + '#6ED0E0', // 2: light blue + '#EF843C', // 3: orange + '#E24D42', // 4: red + '#1F78C1', // 5: ocean + '#BA43A9', // 6: purple + '#705DA0', // 7: violet + '#508642', // 8: dark green + '#CCA300', // 9: dark sand '#447EBC', '#C15C17', '#890F02', diff --git a/public/app/plugins/datasource/logging/result_transformer.ts b/public/app/plugins/datasource/logging/result_transformer.ts index e1e622aeb59..61c0feb493c 100644 --- a/public/app/plugins/datasource/logging/result_transformer.ts +++ b/public/app/plugins/datasource/logging/result_transformer.ts @@ -1,13 +1,12 @@ import _ from 'lodash'; import moment from 'moment'; -import { LogLevel, LogsMetaItem, LogsModel, LogRow, LogsStream } from 'app/core/logs_model'; +import { LogLevel, LogLevelColor, LogsMetaItem, LogsModel, LogRow, LogsStream } from 'app/core/logs_model'; import { TimeSeries } from 'app/core/core'; -import colors from 'app/core/utils/colors'; export function getLogLevel(line: string): LogLevel { if (!line) { - return undefined; + return LogLevel.none; } let level: LogLevel; Object.keys(LogLevel).forEach(key => { @@ -18,6 +17,9 @@ export function getLogLevel(line: string): LogLevel { } } }); + if (!level) { + level = LogLevel.none; + } return level; } @@ -107,8 +109,13 @@ export function mergeStreams(streams: LogsStream[], limit?: number): LogsModel { }, ]; + let intervalMs; + // Flatten entries of streams - const combinedEntries = streams.reduce((acc, stream) => { + const combinedEntries: LogRow[] = streams.reduce((acc, stream) => { + // Set interval for graphs + intervalMs = stream.intervalMs; + // Overwrite labels to be only the non-common ones const labels = formatLabels(findUncommonLabels(stream.parsedLabels, commonLabels)); return [ @@ -120,15 +127,34 @@ export function mergeStreams(streams: LogsStream[], limit?: number): LogsModel { ]; }, []); - const commonLabelsAlias = - streams.length === 1 ? formatLabels(commonLabels) : `Stream with common labels ${formatLabels(commonLabels)}`; - const series = streams.map((stream, index) => { - const colorIndex = index % colors.length; - stream.graphSeries.setColor(colors[colorIndex]); - stream.graphSeries.alias = formatLabels(findUncommonLabels(stream.parsedLabels, commonLabels), commonLabelsAlias); - return stream.graphSeries; + // Graph time series by log level + const seriesByLevel = {}; + combinedEntries.forEach(entry => { + if (!seriesByLevel[entry.logLevel]) { + seriesByLevel[entry.logLevel] = { lastTs: null, datapoints: [], alias: entry.logLevel }; + } + const levelSeries = seriesByLevel[entry.logLevel]; + + // Bucket to nearest minute + const time = Math.round(entry.timeJs / intervalMs / 10) * intervalMs * 10; + // Entry for time + if (time === levelSeries.lastTs) { + levelSeries.datapoints[levelSeries.datapoints.length - 1][0]++; + } else { + levelSeries.datapoints.push([1, time]); + levelSeries.lastTs = time; + } }); + const series = Object.keys(seriesByLevel).reduce((acc, level, index) => { + if (seriesByLevel[level]) { + const gs = new TimeSeries(seriesByLevel[level]); + gs.setColor(LogLevelColor[level]); + acc.push(gs); + } + return acc; + }, []); + const sortedEntries = _.chain(combinedEntries) .sortBy('timestamp') .reverse() @@ -151,25 +177,9 @@ export function processStream(stream: LogsStream, limit?: number, intervalMs?: n .slice(0, limit || stream.entries.length) .value(); - // Build graph data - let previousTime; - const datapoints = sortedEntries.reduce((acc, entry, index) => { - // Bucket to nearest minute - const time = Math.round(entry.timeJs / intervalMs / 10) * intervalMs * 10; - // Entry for time - if (time === previousTime) { - acc[acc.length - 1][0]++; - } else { - acc.push([1, time]); - previousTime = time; - } - return acc; - }, []); - const graphSeries = new TimeSeries({ datapoints, alias: stream.labels }); - return { ...stream, - graphSeries, + intervalMs, entries: sortedEntries, parsedLabels: parseLabels(stream.labels), }; From f5575459ebe76c56ba4ea6a5bdf88f7930775a6f Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 6 Nov 2018 12:05:10 +0100 Subject: [PATCH 063/116] unify log level colors between rows and graph --- public/sass/pages/_explore.scss | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 70b1901fb50..0ca6d17fcf0 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -270,18 +270,26 @@ opacity: 0.8; } - .logs-row-level-crit, + .logs-row-level-crit { + background-color: #705da0; + } + .logs-row-level-error, .logs-row-level-err { - background-color: $red; + background-color: #e24d42; } .logs-row-level-warn { - background-color: $orange; + background-color: #eab839; } .logs-row-level-info { - background-color: $green; + background-color: #7eb26d; + } + + .logs-row-level-trace, + .logs-row-level-debug { + background-color: #1f78c1; } } } From a66dba160811286bd3c40b1840ff4689ed865436 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 6 Nov 2018 13:47:05 +0100 Subject: [PATCH 064/116] changelog: add notes about closing #13932 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb947317f64..756b0778cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322) * **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769) +* **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932) # 5.3.2 (2018-10-24) From b28b79100ab2e404539a9843b4002652a4939347 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 6 Nov 2018 13:50:30 +0100 Subject: [PATCH 065/116] changelog: add notes about closing #13903 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 756b0778cee..a98fca96fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322) * **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769) * **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932) +* **Graph**: Fix table legend height when positioned below graph and using Internet Explorer 11 [#13903](https://github.com/grafana/grafana/issues/13903) # 5.3.2 (2018-10-24) From 9f6683de2c67bf1db3af798c9146cbbfeb321c66 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 25 Oct 2018 12:47:09 +0200 Subject: [PATCH 066/116] wip: Initial commit for PanelHeaderMenu --- .../features/dashboard/dashgrid/DataPanel.tsx | 1 - .../dashboard/dashgrid/PanelChrome.tsx | 2 +- .../dashboard/dashgrid/PanelHeader.tsx | 83 ------------- .../dashgrid/PanelHeader/PanelHeader.tsx | 50 ++++++++ .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 109 ++++++++++++++++++ .../PanelHeader/PanelHeaderMenuItem.tsx | 34 ++++++ public/sass/components/_dropdown.scss | 5 + public/sass/pages/_dashboard.scss | 1 - 8 files changed, 199 insertions(+), 86 deletions(-) delete mode 100644 public/app/features/dashboard/dashgrid/PanelHeader.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index d0122363668..a42d392c018 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -38,7 +38,6 @@ export class DataPanel extends Component { constructor(props: Props) { super(props); - this.state = { loading: LoadingState.NotStarted, response: { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 953dfd62368..d4bfce67c48 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -5,7 +5,7 @@ import React, { ComponentClass, PureComponent } from 'react'; import { getTimeSrv } from '../time_srv'; // Components -import { PanelHeader } from './PanelHeader'; +import { PanelHeader } from './PanelHeader/PanelHeader'; import { DataPanel } from './DataPanel'; // Types diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx deleted file mode 100644 index 12d5cd37253..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelHeader.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { PanelModel } from '../panel_model'; -import { DashboardModel } from '../dashboard_model'; -import { store } from 'app/store/configureStore'; -import { updateLocation } from 'app/core/actions'; - -interface PanelHeaderProps { - panel: PanelModel; - dashboard: DashboardModel; -} - -export class PanelHeader extends React.Component { - onEditPanel = () => { - store.dispatch( - updateLocation({ - query: { - panelId: this.props.panel.id, - edit: true, - fullscreen: true, - }, - }) - ); - }; - - onViewPanel = () => { - store.dispatch( - updateLocation({ - query: { - panelId: this.props.panel.id, - edit: false, - fullscreen: true, - }, - }) - ); - }; - - render() { - const isFullscreen = false; - const isLoading = false; - const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - - return ( -
- - - - - - {isLoading && ( - - - - )} - -
- - - {this.props.panel.title} - - - - - - 4m - - -
-
- ); - } -} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx new file mode 100644 index 00000000000..a5e30d9396e --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import classNames from 'classnames'; +import { PanelModel } from 'app/features/dashboard/panel_model'; +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +// import { store } from 'app/store/configureStore'; +// import { updateLocation } from 'app/core/actions'; +import { PanelHeaderMenu } from './PanelHeaderMenu'; +// import appEvents from 'app/core/app_events'; + +interface PanelHeaderProps { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class PanelHeader extends React.Component { + render() { + const isFullscreen = false; + const isLoading = false; + const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + + return ( +
+ + + + + + {isLoading && ( + + + + )} + +
+
+ + + {this.props.panel.title} + + + + + 4m + +
+
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx new file mode 100644 index 00000000000..6bc6bb54509 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -0,0 +1,109 @@ +import React, { PureComponent } from 'react'; +// import { store } from 'app/store/configureStore'; +import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; +import appEvents from 'app/core/app_events'; +import { store } from 'app/store/configureStore'; +import { updateLocation } from 'app/core/actions'; + +export interface PanelHeaderMenuProps { + panelId: number; +} + +export class PanelHeaderMenu extends PureComponent { + onEditPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: this.props.panelId, + edit: true, + fullscreen: true, + }, + }) + ); + }; + + onViewPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: this.props.panelId, + edit: false, + fullscreen: true, + }, + }) + ); + }; + + onRemovePanel = () => { + appEvents.emit('panel-remove', { + panelId: this.props.panelId, + }); + }; + + render() { + return ( +
+
    + + + {}} + shortcut="p s" + /> + {}} + > +
      + {}} + shortcut="p d" + /> + + {}} /> + + {}} /> + + {}} /> + + {}} + shortcut="p l" + /> +
    +
    + + +
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx new file mode 100644 index 00000000000..3eb4e72ca9d --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx @@ -0,0 +1,34 @@ +import React, { SFC } from 'react'; + +export enum PanelHeaderMenuItemTypes { + Button = 'Button', // ? + Divider = 'Divider', + Link = 'Link', + SubMenu = 'SubMenu', +} + +export interface PanelHeaderMenuItemProps { + type: PanelHeaderMenuItemTypes; + text?: string; + iconClassName?: string; + handleClick?: () => void; + shortcut?: string; + children?: any; +} + +export const PanelHeaderMenuItem: SFC = props => { + const isSubMenu = props.type === PanelHeaderMenuItemTypes.SubMenu; + const isDivider = props.type === PanelHeaderMenuItemTypes.Divider; + return isDivider ? ( +
  • + ) : ( +
  • + + {props.iconClassName && } + {props.text} + {props.shortcut && {props.shortcut}} + + {props.children} +
  • + ); +}; diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss index 37dbdcd89ef..9e7f46fe514 100644 --- a/public/sass/components/_dropdown.scss +++ b/public/sass/components/_dropdown.scss @@ -183,6 +183,11 @@ display: block; } + & > .dropdown > .dropdown-menu { + // Panel menu. TODO: See if we can merge this with above + display: block; + } + &.cascade-open { .dropdown-menu { display: block; diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 795766a22de..125edac500f 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -138,7 +138,6 @@ div.flot-text { padding: 3px 5px; visibility: hidden; opacity: 0; - position: absolute; width: 16px; height: 16px; left: 1px; From 212c086162c0f3f55e7eba10216c769c6a051e47 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 25 Oct 2018 13:57:23 +0200 Subject: [PATCH 067/116] Mobx is now Redux --- public/app/core/services/bridge_srv.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index ee184c243ac..1c91673495d 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -4,7 +4,7 @@ import { store } from 'app/store/configureStore'; import locationUtil from 'app/core/utils/location_util'; import { updateLocation } from 'app/core/actions'; -// Services that handles angular -> mobx store sync & other react <-> angular sync +// Services that handles angular -> redux store sync & other react <-> angular sync export class BridgeSrv { private fullPageReloadRoutes; From 820e47b4c0519f5bf1b8b926bef288080f7c0400 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 25 Oct 2018 13:58:26 +0200 Subject: [PATCH 068/116] wip: panel-header: Remove panel --- .../dashgrid/PanelHeader/PanelHeader.tsx | 3 +- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index a5e30d9396e..ae04f0f0405 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -14,6 +14,7 @@ interface PanelHeaderProps { export class PanelHeader extends React.Component { render() { + const { dashboard } = this.props; const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); @@ -38,7 +39,7 @@ export class PanelHeader extends React.Component { {this.props.panel.title} - + 4m diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 6bc6bb54509..19fee872e4b 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,5 +1,7 @@ import React, { PureComponent } from 'react'; // import { store } from 'app/store/configureStore'; +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import appEvents from 'app/core/app_events'; import { store } from 'app/store/configureStore'; @@ -7,6 +9,7 @@ import { updateLocation } from 'app/core/actions'; export interface PanelHeaderMenuProps { panelId: number; + dashboard: DashboardModel; } export class PanelHeaderMenu extends PureComponent { @@ -35,9 +38,32 @@ export class PanelHeaderMenu extends PureComponent { }; onRemovePanel = () => { - appEvents.emit('panel-remove', { - panelId: this.props.panelId, - }); + const { panelId, dashboard } = this.props; + const panelInfo = dashboard.getPanelInfoById(panelId); + this.removePanel(panelInfo.panel, true); + }; + + removePanel = (panel: PanelModel, ask: boolean) => { + const { dashboard } = this.props; + + // confirm deletion + if (ask !== false) { + const text2 = panel.alert ? 'Panel includes an alert rule, removing panel will also remove alert rule' : null; + const confirmText = panel.alert ? 'YES' : null; + + appEvents.emit('confirm-modal', { + title: 'Remove Panel', + text: 'Are you sure you want to remove this panel?', + text2: text2, + icon: 'fa-trash', + confirmText: confirmText, + yesText: 'Remove', + onConfirm: () => this.removePanel(panel, false), + }); + return; + } + + dashboard.removePanel(panel); }; render() { From bf8703edb88664e15f6244d53a778f7fc6bff5f2 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 25 Oct 2018 14:29:03 +0200 Subject: [PATCH 069/116] wip: panel-header: Move code existing in both angular+react to utility functions --- .../app/features/dashboard/dashboard_ctrl.ts | 31 ++--------------- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 33 ++----------------- public/app/features/dashboard/utils/panel.ts | 27 +++++++++++++++ 3 files changed, 31 insertions(+), 60 deletions(-) create mode 100644 public/app/features/dashboard/utils/panel.ts diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 5871a579f3c..60517df19f6 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -2,13 +2,13 @@ import config from 'app/core/config'; import appEvents from 'app/core/app_events'; import coreModule from 'app/core/core_module'; +import { removePanel } from 'app/features/dashboard/utils/panel'; // Services import { AnnotationsSrv } from '../annotations/annotations_srv'; // Types import { DashboardModel } from './dashboard_model'; -import { PanelModel } from './panel_model'; export class DashboardCtrl { dashboard: DashboardModel; @@ -136,34 +136,7 @@ export class DashboardCtrl { } const panelInfo = this.dashboard.getPanelInfoById(options.panelId); - this.removePanel(panelInfo.panel, true); - } - - removePanel(panel: PanelModel, ask: boolean) { - // confirm deletion - if (ask !== false) { - let text2, confirmText; - - if (panel.alert) { - text2 = 'Panel includes an alert rule, removing panel will also remove alert rule'; - confirmText = 'YES'; - } - - this.$scope.appEvent('confirm-modal', { - title: 'Remove Panel', - text: 'Are you sure you want to remove this panel?', - text2: text2, - icon: 'fa-trash', - confirmText: confirmText, - yesText: 'Remove', - onConfirm: () => { - this.removePanel(panel, false); - }, - }); - return; - } - - this.dashboard.removePanel(panel); + removePanel(this.dashboard, panelInfo.panel, true); } onDestroy() { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 19fee872e4b..3a27796bb90 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,11 +1,9 @@ import React, { PureComponent } from 'react'; -// import { store } from 'app/store/configureStore'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; -import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; -import appEvents from 'app/core/app_events'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; +import { removePanel } from 'app/features/dashboard/utils/panel'; export interface PanelHeaderMenuProps { panelId: number; @@ -40,30 +38,7 @@ export class PanelHeaderMenu extends PureComponent { onRemovePanel = () => { const { panelId, dashboard } = this.props; const panelInfo = dashboard.getPanelInfoById(panelId); - this.removePanel(panelInfo.panel, true); - }; - - removePanel = (panel: PanelModel, ask: boolean) => { - const { dashboard } = this.props; - - // confirm deletion - if (ask !== false) { - const text2 = panel.alert ? 'Panel includes an alert rule, removing panel will also remove alert rule' : null; - const confirmText = panel.alert ? 'YES' : null; - - appEvents.emit('confirm-modal', { - title: 'Remove Panel', - text: 'Are you sure you want to remove this panel?', - text2: text2, - icon: 'fa-trash', - confirmText: confirmText, - yesText: 'Remove', - onConfirm: () => this.removePanel(panel, false), - }); - return; - } - - dashboard.removePanel(panel); + removePanel(dashboard, panelInfo.panel, true); }; render() { @@ -105,13 +80,9 @@ export class PanelHeaderMenu extends PureComponent { handleClick={() => {}} shortcut="p d" /> - {}} /> - {}} /> - {}} /> - { + // confirm deletion + if (ask !== false) { + const text2 = panel.alert ? 'Panel includes an alert rule, removing panel will also remove alert rule' : null; + const confirmText = panel.alert ? 'YES' : null; + + appEvents.emit('confirm-modal', { + title: 'Remove Panel', + text: 'Are you sure you want to remove this panel?', + text2: text2, + icon: 'fa-trash', + confirmText: confirmText, + yesText: 'Remove', + onConfirm: () => removePanel(dashboard, panel, false), + }); + return; + } + dashboard.removePanel(panel); +}; + +export default { + removePanel, +}; From 839057dc7a393667b231b42fb4d7451a0db77782 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 30 Oct 2018 14:21:47 +0100 Subject: [PATCH 070/116] wip: Add "Share" to the react panels --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 3a27796bb90..5e6f5a5d28c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -4,6 +4,7 @@ import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenu import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; import { removePanel } from 'app/features/dashboard/utils/panel'; +import appEvents from 'app/core/app_events'; export interface PanelHeaderMenuProps { panelId: number; @@ -11,6 +12,13 @@ export interface PanelHeaderMenuProps { } export class PanelHeaderMenu extends PureComponent { + getPanel = () => { + // Pass in panel as prop instead? + const { panelId, dashboard } = this.props; + const panelInfo = dashboard.getPanelInfoById(panelId); + return panelInfo.panel; + }; + onEditPanel = () => { store.dispatch( updateLocation({ @@ -36,9 +44,22 @@ export class PanelHeaderMenu extends PureComponent { }; onRemovePanel = () => { - const { panelId, dashboard } = this.props; - const panelInfo = dashboard.getPanelInfoById(panelId); - removePanel(dashboard, panelInfo.panel, true); + const { dashboard } = this.props; + const panel = this.getPanel(); + removePanel(dashboard, panel, true); + }; + + onSharePanel = () => { + const { dashboard } = this.props; + const panel = this.getPanel(); + + appEvents.emit('show-modal', { + src: 'public/app/features/dashboard/partials/shareModal.html', + model: { + panel: panel, + dashboard: dashboard, + }, + }); }; render() { @@ -63,7 +84,7 @@ export class PanelHeaderMenu extends PureComponent { type={PanelHeaderMenuItemTypes.Link} text="Share" iconClassName="fa fa-fw fa-share" - handleClick={() => {}} + handleClick={this.onSharePanel} shortcut="p s" /> Date: Tue, 30 Oct 2018 14:38:18 +0100 Subject: [PATCH 071/116] wip: panel-header: Add "Duplicate" --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 11 +++++++++-- public/app/features/dashboard/utils/panel.ts | 5 +++++ public/app/features/panel/panel_ctrl.ts | 3 ++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 5e6f5a5d28c..49adfbb703a 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel } from 'app/features/dashboard/utils/panel'; +import { removePanel, duplicatePanel } from 'app/features/dashboard/utils/panel'; import appEvents from 'app/core/app_events'; export interface PanelHeaderMenuProps { @@ -62,6 +62,13 @@ export class PanelHeaderMenu extends PureComponent { }); }; + onDuplicatePanel = () => { + const { dashboard } = this.props; + const panel = this.getPanel(); + + duplicatePanel(dashboard, panel); + }; + render() { return (
    @@ -98,7 +105,7 @@ export class PanelHeaderMenu extends PureComponent { type={PanelHeaderMenuItemTypes.Link} text="Duplicate" iconClassName="" - handleClick={() => {}} + handleClick={this.onDuplicatePanel} shortcut="p d" /> {}} /> diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 6257aae232e..35ccfdf3b3f 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -22,6 +22,11 @@ export const removePanel = (dashboard: DashboardModel, panel: PanelModel, ask: b dashboard.removePanel(panel); }; +export const duplicatePanel = (dashboard: DashboardModel, panel: PanelModel) => { + dashboard.duplicatePanel(panel); +}; + export default { removePanel, + duplicatePanel, }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 08605132e82..d1e2dcc20cf 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import $ from 'jquery'; import { appEvents, profiler } from 'app/core/core'; import { PanelModel } from 'app/features/dashboard/panel_model'; +import { duplicatePanel } from 'app/features/dashboard/utils/panel'; import Remarkable from 'remarkable'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, LS_PANEL_COPY_KEY } from 'app/core/constants'; import store from 'app/core/store'; @@ -241,7 +242,7 @@ export class PanelCtrl { } duplicate() { - this.dashboard.duplicatePanel(this.panel); + duplicatePanel(this.dashboard, this.panel); } removePanel() { From edceb204e7a552b1ebd82149b76b78de96474cec Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 30 Oct 2018 14:53:04 +0100 Subject: [PATCH 072/116] wip: panel-header: Add "Copy" functionality --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 11 ++++++++--- public/app/features/dashboard/utils/panel.ts | 8 ++++++++ public/app/features/panel/panel_ctrl.ts | 10 ++++------ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 49adfbb703a..190c13ead9d 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel, duplicatePanel } from 'app/features/dashboard/utils/panel'; +import { removePanel, duplicatePanel, copyPanel } from 'app/features/dashboard/utils/panel'; import appEvents from 'app/core/app_events'; export interface PanelHeaderMenuProps { @@ -69,6 +69,11 @@ export class PanelHeaderMenu extends PureComponent { duplicatePanel(dashboard, panel); }; + onCopyPanel = () => { + const panel = this.getPanel(); + copyPanel(panel); + }; + render() { return (
    @@ -98,7 +103,7 @@ export class PanelHeaderMenu extends PureComponent { type={PanelHeaderMenuItemTypes.SubMenu} text="More ..." iconClassName="fa fa-fw fa-cube" - handleClick={() => {}} + handleClick={null} >
      { handleClick={this.onDuplicatePanel} shortcut="p d" /> - {}} /> + {}} /> {}} /> { // confirm deletion @@ -26,7 +28,13 @@ export const duplicatePanel = (dashboard: DashboardModel, panel: PanelModel) => dashboard.duplicatePanel(panel); }; +export const copyPanel = (panel: PanelModel) => { + store.set(LS_PANEL_COPY_KEY, JSON.stringify(panel.getSaveModel())); + appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']); +}; + export default { removePanel, duplicatePanel, + copyPanel, }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index d1e2dcc20cf..cb35b5ef470 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -1,12 +1,11 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; -import { appEvents, profiler } from 'app/core/core'; +import { profiler } from 'app/core/core'; import { PanelModel } from 'app/features/dashboard/panel_model'; -import { duplicatePanel } from 'app/features/dashboard/utils/panel'; +import { duplicatePanel, copyPanel } from 'app/features/dashboard/utils/panel'; import Remarkable from 'remarkable'; -import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, LS_PANEL_COPY_KEY } from 'app/core/constants'; -import store from 'app/core/store'; +import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; const TITLE_HEIGHT = 27; const PANEL_BORDER = 2; @@ -264,8 +263,7 @@ export class PanelCtrl { } copyPanel() { - store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel())); - appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']); + copyPanel(this.panel); } replacePanel(newPanel, oldPanel) { From f9dd5165782e9841f464dfaf3fa1205ebaa7b7cc Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 30 Oct 2018 16:07:59 +0100 Subject: [PATCH 073/116] wip: panel-header: Add "Edit JSON" functionality + make sure everyone using the json editor pass in the model property instead of the scope property when triggering the json modal --- .../app/core/controllers/json_editor_ctrl.ts | 8 ++--- .../app/features/dashboard/dashboard_ctrl.ts | 11 +++--- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 14 ++++++-- .../features/dashboard/export/export_modal.ts | 9 ++--- public/app/features/dashboard/utils/panel.ts | 33 ++++++++++++++--- public/app/features/panel/panel_ctrl.ts | 36 ++++--------------- 6 files changed, 63 insertions(+), 48 deletions(-) diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index 9c3f9d9e98d..7439433c55e 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -4,13 +4,13 @@ import coreModule from '../core_module'; export class JsonEditorCtrl { /** @ngInject */ constructor($scope) { - $scope.json = angular.toJson($scope.object, true); - $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor; - $scope.canCopy = $scope.enableCopy; + $scope.json = angular.toJson($scope.model.object, true); + $scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.contextSrv.isEditor; + $scope.canCopy = $scope.model.enableCopy; $scope.update = () => { const newObject = angular.fromJson($scope.json); - $scope.updateHandler(newObject, $scope.object); + $scope.model.updateHandler(newObject, $scope.model.object); }; $scope.getContentForClipboard = () => $scope.json; diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 60517df19f6..6611a728803 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -19,7 +19,6 @@ export class DashboardCtrl { /** @ngInject */ constructor( private $scope, - private $rootScope, private keybindingSrv, private timeSrv, private variableSrv, @@ -112,12 +111,14 @@ export class DashboardCtrl { } showJsonEditor(evt, options) { - const editScope = this.$rootScope.$new(); - editScope.object = options.object; - editScope.updateHandler = options.updateHandler; + const model = { + object: options.object, + updateHandler: options.updateHandler, + }; + this.$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', - scope: editScope, + model: model, }); } diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 190c13ead9d..9ac1e91483c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel, duplicatePanel, copyPanel } from 'app/features/dashboard/utils/panel'; +import { removePanel, duplicatePanel, copyPanel, editPanelJson } from 'app/features/dashboard/utils/panel'; import appEvents from 'app/core/app_events'; export interface PanelHeaderMenuProps { @@ -74,6 +74,12 @@ export class PanelHeaderMenu extends PureComponent { copyPanel(panel); }; + onEditPanelJson = () => { + const { dashboard } = this.props; + const panel = this.getPanel(); + editPanelJson(dashboard, panel); + }; + render() { return (
      @@ -114,7 +120,11 @@ export class PanelHeaderMenu extends PureComponent { shortcut="p d" /> - {}} /> + {}} /> { appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']); }; -export default { - removePanel, - duplicatePanel, - copyPanel, +const replacePanel = (dashboard: DashboardModel, newPanel: PanelModel, oldPanel: PanelModel) => { + const index = dashboard.panels.findIndex(panel => { + return panel.id === oldPanel.id; + }); + + const deletedPanel = dashboard.panels.splice(index, 1); + dashboard.events.emit('panel-removed', deletedPanel); + + newPanel = new PanelModel(newPanel); + newPanel.id = oldPanel.id; + + dashboard.panels.splice(index, 0, newPanel); + dashboard.sortPanelsByGridPos(); + dashboard.events.emit('panel-added', newPanel); +}; + +export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => { + const model = { + object: panel.getSaveModel(), + updateHandler: (newPanel: PanelModel, oldPanel: PanelModel) => { + replacePanel(dashboard, newPanel, oldPanel); + }, + enableCopy: true, + }; + + appEvents.emit('show-modal', { + src: 'public/app/partials/edit_json.html', + model: model, + }); }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index cb35b5ef470..169ec8b322b 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -2,8 +2,11 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; import { profiler } from 'app/core/core'; -import { PanelModel } from 'app/features/dashboard/panel_model'; -import { duplicatePanel, copyPanel } from 'app/features/dashboard/utils/panel'; +import { + duplicatePanel, + copyPanel as copyPanelUtil, + editPanelJson as editPanelJsonUtil, +} from 'app/features/dashboard/utils/panel'; import Remarkable from 'remarkable'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; @@ -251,36 +254,11 @@ export class PanelCtrl { } editPanelJson() { - const editScope = this.$scope.$root.$new(); - editScope.object = this.panel.getSaveModel(); - editScope.updateHandler = this.replacePanel.bind(this); - editScope.enableCopy = true; - - this.publishAppEvent('show-modal', { - src: 'public/app/partials/edit_json.html', - scope: editScope, - }); + editPanelJsonUtil(this.dashboard, this.panel); } copyPanel() { - copyPanel(this.panel); - } - - replacePanel(newPanel, oldPanel) { - const dashboard = this.dashboard; - const index = _.findIndex(dashboard.panels, panel => { - return panel.id === oldPanel.id; - }); - - const deletedPanel = dashboard.panels.splice(index, 1); - this.dashboard.events.emit('panel-removed', deletedPanel); - - newPanel = new PanelModel(newPanel); - newPanel.id = oldPanel.id; - - dashboard.panels.splice(index, 0, newPanel); - dashboard.sortPanelsByGridPos(); - dashboard.events.emit('panel-added', newPanel); + copyPanelUtil(this.panel); } sharePanel() { From 5375ce5ffdcc7b6588edcdc9c06c0ad8c5563f71 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 30 Oct 2018 16:39:08 +0100 Subject: [PATCH 074/116] wip: panel-header: Refactor so "Share" use the same code in angular+react --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 11 ++--------- public/app/features/dashboard/shareModalCtrl.ts | 2 ++ public/app/features/dashboard/utils/panel.ts | 10 ++++++++++ public/app/features/panel/panel_ctrl.ts | 10 ++-------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 9ac1e91483c..13b0fca86e9 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,8 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel, duplicatePanel, copyPanel, editPanelJson } from 'app/features/dashboard/utils/panel'; -import appEvents from 'app/core/app_events'; +import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; export interface PanelHeaderMenuProps { panelId: number; @@ -53,13 +52,7 @@ export class PanelHeaderMenu extends PureComponent { const { dashboard } = this.props; const panel = this.getPanel(); - appEvents.emit('show-modal', { - src: 'public/app/features/dashboard/partials/shareModal.html', - model: { - panel: panel, - dashboard: dashboard, - }, - }); + sharePanel(dashboard, panel); }; onDuplicatePanel = () => { diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index c00a6d8d57f..f894d24202f 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -12,6 +12,8 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, $scope.editor = { index: $scope.tabIndex || 0 }; $scope.init = () => { + $scope.panel = $scope.model.panel || $scope.panel; // React pass panel and dashboard in the "model" property + $scope.dashboard = $scope.model.dashboard || $scope.dashboard; $scope.modeSharePanel = $scope.panel ? true : false; $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }]; diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 7f89090c336..fff837d9685 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -63,3 +63,13 @@ export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => { model: model, }); }; + +export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => { + appEvents.emit('show-modal', { + src: 'public/app/features/dashboard/partials/shareModal.html', + model: { + dashboard: dashboard, + panel: panel, + }, + }); +}; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 169ec8b322b..92932142690 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -6,6 +6,7 @@ import { duplicatePanel, copyPanel as copyPanelUtil, editPanelJson as editPanelJsonUtil, + sharePanel as sharePanelUtil, } from 'app/features/dashboard/utils/panel'; import Remarkable from 'remarkable'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; @@ -262,14 +263,7 @@ export class PanelCtrl { } sharePanel() { - const shareScope = this.$scope.$new(); - shareScope.panel = this.panel; - shareScope.dashboard = this.dashboard; - - this.publishAppEvent('show-modal', { - src: 'public/app/features/dashboard/partials/shareModal.html', - scope: shareScope, - }); + sharePanelUtil(this.dashboard, this.panel); } getInfoMode() { From 79da3dc9f6f89576727fc966a615051b1663c25c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 13:41:50 +0100 Subject: [PATCH 075/116] wip: panel-header: Change DashboardPanel to a PureComponent to avoid unwanted rerenders --- public/app/features/dashboard/dashgrid/DashboardPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 7dd8a06996d..fcfc84e287b 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; @@ -20,7 +20,7 @@ export interface State { pluginExports: PluginExports; } -export class DashboardPanel extends React.Component { +export class DashboardPanel extends PureComponent { element: any; angularPanel: AngularComponent; pluginInfo: any; From 61513102169f05d7955d34254830c85cd302cf01 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 13:43:21 +0100 Subject: [PATCH 076/116] wip: panel-header: Start implementing the Toggle legend, but its not taken all the way --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 16 ++++++++++++++-- public/app/features/dashboard/utils/panel.ts | 11 +++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 13b0fca86e9..826406fddff 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,7 +3,14 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; +import { + removePanel, + duplicatePanel, + copyPanel, + editPanelJson, + sharePanel, + toggleLegend, +} from 'app/features/dashboard/utils/panel'; export interface PanelHeaderMenuProps { panelId: number; @@ -73,6 +80,11 @@ export class PanelHeaderMenu extends PureComponent { editPanelJson(dashboard, panel); }; + onToggleLegend = () => { + const panel = this.getPanel(); + toggleLegend(panel); + }; + render() { return (
      @@ -122,7 +134,7 @@ export class PanelHeaderMenu extends PureComponent { {}} + handleClick={this.onToggleLegend} shortcut="p l" />
    diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index fff837d9685..151c1ea8d61 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -73,3 +73,14 @@ export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => { }, }); }; + +export const refreshPanel = (panel: PanelModel) => { + panel.refresh(); +}; + +export const toggleLegend = (panel: PanelModel) => { + console.log('Toggle legend is not implemented yet'); + // We need to set panel.legend defaults first + // panel.legend.show = !panel.legend.show; + refreshPanel(panel); +}; From f124b9de6a0c12125d420c25f7e2625901b05af6 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 16:04:14 +0100 Subject: [PATCH 077/116] wip: panel-header: Separate all panel actions to its own file so we decouple them from react --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 159 +++--------------- .../PanelHeader/PanelHeaderMenuItem.tsx | 4 +- .../features/dashboard/utils/panel_menu.ts | 140 +++++++++++++++ 3 files changed, 169 insertions(+), 134 deletions(-) create mode 100644 public/app/features/dashboard/utils/panel_menu.ts diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 826406fddff..adce83e8c40 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,16 +1,7 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, Fragment } from 'react'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; -import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; -import { store } from 'app/store/configureStore'; -import { updateLocation } from 'app/core/actions'; -import { - removePanel, - duplicatePanel, - copyPanel, - editPanelJson, - sharePanel, - toggleLegend, -} from 'app/features/dashboard/utils/panel'; +import { PanelHeaderMenuItem, PanelHeaderMenuItemProps } from './PanelHeaderMenuItem'; +import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; export interface PanelHeaderMenuProps { panelId: number; @@ -25,130 +16,32 @@ export class PanelHeaderMenu extends PureComponent { return panelInfo.panel; }; - onEditPanel = () => { - store.dispatch( - updateLocation({ - query: { - panelId: this.props.panelId, - edit: true, - fullscreen: true, - }, - }) + renderItems = (menu: PanelHeaderMenuItemProps[], isSubMenu = false) => { + return ( +
      + {menu.map(menuItem => { + console.log(this); + return ( + + + {menuItem.subMenu && this.renderItems(menuItem.subMenu, true)} + + + ); + })} +
    ); }; - onViewPanel = () => { - store.dispatch( - updateLocation({ - query: { - panelId: this.props.panelId, - edit: false, - fullscreen: true, - }, - }) - ); - }; - - onRemovePanel = () => { - const { dashboard } = this.props; - const panel = this.getPanel(); - removePanel(dashboard, panel, true); - }; - - onSharePanel = () => { - const { dashboard } = this.props; - const panel = this.getPanel(); - - sharePanel(dashboard, panel); - }; - - onDuplicatePanel = () => { - const { dashboard } = this.props; - const panel = this.getPanel(); - - duplicatePanel(dashboard, panel); - }; - - onCopyPanel = () => { - const panel = this.getPanel(); - copyPanel(panel); - }; - - onEditPanelJson = () => { - const { dashboard } = this.props; - const panel = this.getPanel(); - editPanelJson(dashboard, panel); - }; - - onToggleLegend = () => { - const panel = this.getPanel(); - toggleLegend(panel); - }; - render() { - return ( -
    -
      - - - - -
        - - - - {}} /> - -
      -
      - - -
    -
    - ); + const { dashboard } = this.props; + const menu = getPanelMenu(dashboard, this.getPanel()); + return
    {this.renderItems(menu)}
    ; } } diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx index 3eb4e72ca9d..f0b5579c2a1 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx @@ -1,6 +1,6 @@ import React, { SFC } from 'react'; -export enum PanelHeaderMenuItemTypes { +export enum PanelHeaderMenuItemTypes { // TODO: Evaluate. Remove? Button = 'Button', // ? Divider = 'Divider', Link = 'Link', @@ -14,6 +14,8 @@ export interface PanelHeaderMenuItemProps { handleClick?: () => void; shortcut?: string; children?: any; + subMenu?: PanelHeaderMenuItemProps[]; + role?: string; } export const PanelHeaderMenuItem: SFC = props => { diff --git a/public/app/features/dashboard/utils/panel_menu.ts b/public/app/features/dashboard/utils/panel_menu.ts new file mode 100644 index 00000000000..de2ba852d13 --- /dev/null +++ b/public/app/features/dashboard/utils/panel_menu.ts @@ -0,0 +1,140 @@ +import { PanelHeaderMenuItemTypes, PanelHeaderMenuItemProps } from './../dashgrid/PanelHeader/PanelHeaderMenuItem'; +import { store } from 'app/store/configureStore'; +import { updateLocation } from 'app/core/actions'; +import { PanelModel } from 'app/features/dashboard/panel_model'; +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; + +export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { + const onViewPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: panel.id, + edit: false, + fullscreen: true, + }, + }) + ); + }; + + const onEditPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: panel.id, + edit: true, + fullscreen: true, + }, + }) + ); + }; + + const onSharePanel = () => { + sharePanel(dashboard, panel); + }; + + const onDuplicatePanel = () => { + duplicatePanel(dashboard, panel); + }; + + const onCopyPanel = () => { + copyPanel(panel); + }; + + const onEditPanelJson = () => { + editPanelJson(dashboard, panel); + }; + + const onRemovePanel = () => { + removePanel(dashboard, panel, true); + }; + + const getSubMenu = () => { + const menu: PanelHeaderMenuItemProps[] = []; + + if (!panel.fullscreen && dashboard.meta.canEdit) { + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Duplicate', + handleClick: onDuplicatePanel, + shortcut: 'p d', + role: 'Editor', + }); + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Copy', + handleClick: onCopyPanel, + role: 'Editor', + }); + } + + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Panel JSON', + handleClick: onEditPanelJson, + }); + + // TODO: Handle this somehow + // this.events.emit('init-panel-actions', menu); + return menu; + }; + + const menu: PanelHeaderMenuItemProps[] = []; + + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'View', + iconClassName: 'fa fa-fw fa-eye', + handleClick: onViewPanel, + shortcut: 'v', + }); + + if (dashboard.meta.canEdit) { + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Edit', + iconClassName: 'fa fa-fw fa-edit', + handleClick: onEditPanel, + shortcut: 'e', + role: 'Editor', + }); + } + + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Share', + iconClassName: 'fa fa-fw fa-share', + handleClick: onSharePanel, + shortcut: 'p s', + }); + + const subMenu: PanelHeaderMenuItemProps[] = getSubMenu(); + + menu.push({ + type: PanelHeaderMenuItemTypes.SubMenu, + text: 'More...', + iconClassName: 'fa fa-fw fa-cube', + handleClick: null, + subMenu: subMenu, + }); + + if (dashboard.meta.canEdit) { + menu.push({ + type: PanelHeaderMenuItemTypes.Divider, + role: 'Editor', + }); + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Remove', + iconClassName: 'fa fa-fw fa-trash', + handleClick: onRemovePanel, + shortcut: 'p r', + role: 'Editor', + }); + } + + // Additional items from sub-class + // menu.push(...this.getAdditionalMenuItems()); + return menu; +}; From 443d381dd91444d81fbf614828fd0c6b06f52b7c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 16:19:11 +0100 Subject: [PATCH 078/116] wip: panel-header: Add possibility to add custom actions to the menu by passing them in as props --- public/app/features/dashboard/utils/panel_menu.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/utils/panel_menu.ts b/public/app/features/dashboard/utils/panel_menu.ts index de2ba852d13..c86595953b5 100644 --- a/public/app/features/dashboard/utils/panel_menu.ts +++ b/public/app/features/dashboard/utils/panel_menu.ts @@ -5,7 +5,12 @@ import { PanelModel } from 'app/features/dashboard/panel_model'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; -export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { +export const getPanelMenu = ( + dashboard: DashboardModel, + panel: PanelModel, + extraMenuItems: PanelHeaderMenuItemProps[] = [], + extraSubMenuItems: PanelHeaderMenuItemProps[] = [] +) => { const onViewPanel = () => { store.dispatch( updateLocation({ @@ -77,6 +82,9 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { // TODO: Handle this somehow // this.events.emit('init-panel-actions', menu); + extraSubMenuItems.forEach(item => { + menu.push(item); + }); return menu; }; @@ -109,6 +117,10 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { shortcut: 'p s', }); + extraMenuItems.forEach(item => { + menu.push(item); + }); + const subMenu: PanelHeaderMenuItemProps[] = getSubMenu(); menu.push({ From f471482569a3d391c8c76d8ef6019f9251da9244 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 16:22:48 +0100 Subject: [PATCH 079/116] wip: panel-header: Fragment not needed anymore --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index adce83e8c40..b454ccad4a6 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemProps } from './PanelHeaderMenuItem'; import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; @@ -22,17 +22,15 @@ export class PanelHeaderMenu extends PureComponent { {menu.map(menuItem => { console.log(this); return ( - - - {menuItem.subMenu && this.renderItems(menuItem.subMenu, true)} - - + + {menuItem.subMenu && this.renderItems(menuItem.subMenu, true)} + ); })} From ca4612af261aec49970ad45ca57a2e46e482cb62 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 1 Nov 2018 12:01:27 +0100 Subject: [PATCH 080/116] wip: panel-header: Merge conflicts --- .../dashboard/dashgrid/DashboardPanel.tsx | 3 +- .../dashboard/dashgrid/PanelChrome.tsx | 12 +-- .../dashgrid/PanelHeader/PanelHeader.tsx | 14 ++- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 12 ++- .../features/dashboard/utils/panel_menu.ts | 10 +- public/app/plugins/panel/graph2/module.tsx | 1 + .../plugins/panel/graph2/withMenuOptions.tsx | 94 +++++++++++++++++++ public/app/types/plugins.ts | 1 + 8 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 public/app/plugins/panel/graph2/withMenuOptions.tsx diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index fcfc84e287b..cf41595ce8c 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; @@ -123,6 +123,7 @@ export class DashboardPanel extends PureComponent {
    diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index d4bfce67c48..1a6f5a3cee2 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -13,19 +13,20 @@ import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { TimeRange, PanelProps } from 'app/types'; -export interface Props { +export interface PanelChromeProps { panel: PanelModel; dashboard: DashboardModel; component: ComponentClass; + withMenuOptions: any; } -export interface State { +export interface PanelChromeState { refreshCounter: number; renderCounter: number; timeRange?: TimeRange; } -export class PanelChrome extends PureComponent { +export class PanelChrome extends PureComponent { constructor(props) { super(props); @@ -67,16 +68,15 @@ export class PanelChrome extends PureComponent { } render() { - const { panel, dashboard } = this.props; + const { panel, dashboard, withMenuOptions } = this.props; const { datasource, targets } = panel; const { timeRange, renderCounter, refreshCounter } = this.state; const PanelComponent = this.props.component; - console.log('Panel chrome render'); return (
    - +
    { +export class PanelHeader extends PureComponent { render() { - const { dashboard } = this.props; + const { dashboard, withMenuOptions, panel } = this.props; const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + const PanelHeaderMenuComponent = withMenuOptions ? withMenuOptions(PanelHeaderMenu, panel) : PanelHeaderMenu; return (
    @@ -39,7 +37,7 @@ export class PanelHeader extends React.Component { {this.props.panel.title} - + 4m diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index b454ccad4a6..c36eb9d8584 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -6,6 +6,9 @@ import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; export interface PanelHeaderMenuProps { panelId: number; dashboard: DashboardModel; + datasource: any; + additionalMenuItems?: PanelHeaderMenuItemProps[]; + additionalSubMenuItems?: PanelHeaderMenuItemProps[]; } export class PanelHeaderMenu extends PureComponent { @@ -19,10 +22,10 @@ export class PanelHeaderMenu extends PureComponent { renderItems = (menu: PanelHeaderMenuItemProps[], isSubMenu = false) => { return (
      - {menu.map(menuItem => { - console.log(this); + {menu.map((menuItem, idx) => { return ( { }; render() { - const { dashboard } = this.props; - const menu = getPanelMenu(dashboard, this.getPanel()); + console.log('PanelHeaderMenu render'); + const { dashboard, additionalMenuItems, additionalSubMenuItems } = this.props; + const menu = getPanelMenu(dashboard, this.getPanel(), additionalMenuItems, additionalSubMenuItems); return
      {this.renderItems(menu)}
      ; } } diff --git a/public/app/features/dashboard/utils/panel_menu.ts b/public/app/features/dashboard/utils/panel_menu.ts index c86595953b5..67adf118edd 100644 --- a/public/app/features/dashboard/utils/panel_menu.ts +++ b/public/app/features/dashboard/utils/panel_menu.ts @@ -8,8 +8,8 @@ import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } fro export const getPanelMenu = ( dashboard: DashboardModel, panel: PanelModel, - extraMenuItems: PanelHeaderMenuItemProps[] = [], - extraSubMenuItems: PanelHeaderMenuItemProps[] = [] + additionalMenuItems: PanelHeaderMenuItemProps[] = [], + additionalSubMenuItems: PanelHeaderMenuItemProps[] = [] ) => { const onViewPanel = () => { store.dispatch( @@ -80,9 +80,7 @@ export const getPanelMenu = ( handleClick: onEditPanelJson, }); - // TODO: Handle this somehow - // this.events.emit('init-panel-actions', menu); - extraSubMenuItems.forEach(item => { + additionalSubMenuItems.forEach(item => { menu.push(item); }); return menu; @@ -117,7 +115,7 @@ export const getPanelMenu = ( shortcut: 'p s', }); - extraMenuItems.forEach(item => { + additionalMenuItems.forEach(item => { menu.push(item); }); diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index b132d3374f1..88b679e1645 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -73,3 +73,4 @@ export class GraphOptions extends PureComponent> { } export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent }; +export { withMenuOptions } from './withMenuOptions'; diff --git a/public/app/plugins/panel/graph2/withMenuOptions.tsx b/public/app/plugins/panel/graph2/withMenuOptions.tsx new file mode 100644 index 00000000000..aaa89bf3406 --- /dev/null +++ b/public/app/plugins/panel/graph2/withMenuOptions.tsx @@ -0,0 +1,94 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Services +import { getTimeSrv } from 'app/features/dashboard/time_srv'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { store } from 'app/store/configureStore'; + +// Components +import { PanelHeaderMenu } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu'; +import config from 'app/core/config'; +import { getExploreUrl } from 'app/core/utils/explore'; +import { updateLocation } from 'app/core/actions'; + +// Types +import { PanelModel } from 'app/features/dashboard/panel_model'; +import { PanelHeaderMenuProps } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu'; +import { + PanelHeaderMenuItemProps, + PanelHeaderMenuItemTypes, +} from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; + +interface LocalState { + datasource: any; +} + +export const withMenuOptions = (WrappedPanelHeaderMenu: typeof PanelHeaderMenu, panel: PanelModel) => { + return class extends PureComponent { + private datasourceSrv = getDatasourceSrv(); + private timeSrv = getTimeSrv(); + + constructor(props) { + super(props); + this.state = { + datasource: undefined, + }; + } + + componentDidMount() { + const dsPromise = getDatasourceSrv().get(panel.datasource); + dsPromise.then((datasource: any) => { + this.setState(() => ({ datasource })); + }); + } + + onExploreClick = async () => { + const { datasource } = this.state; + const url = await getExploreUrl(panel, panel.targets, datasource, this.datasourceSrv, this.timeSrv); + if (url) { + store.dispatch(updateLocation({ path: url })); + } + }; + + getAdditionalMenuItems = () => { + const { datasource } = this.state; + const items = []; + if ( + config.exploreEnabled && + contextSrv.isEditor && + datasource && + (datasource.meta.explore || datasource.meta.id === 'mixed') + ) { + items.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Explore', + handleClick: this.onExploreClick, + iconClassName: 'fa fa-fw fa-rocket', + shortcut: 'x', + }); + } + return items; + }; + + getAdditionalSubMenuItems = () => { + return [ + { + type: PanelHeaderMenuItemTypes.Link, + text: 'Hello Sub Menu', + handleClick: () => { + alert('Hello world from HOC!'); + }, + shortcut: 's h w', + }, + ] as PanelHeaderMenuItemProps[]; + }; + + render() { + const menu: PanelHeaderMenuItemProps[] = this.getAdditionalMenuItems(); + const subMenu: PanelHeaderMenuItemProps[] = this.getAdditionalSubMenuItems(); + return ; + } + }; +}; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 817777669d8..9ede3dd9f4b 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -13,6 +13,7 @@ export interface PluginExports { PanelCtrl?; PanelComponent?: ComponentClass; PanelOptionsComponent: ComponentClass; + withMenuOptions?: any; } export interface PanelPlugin { From dfc0c5052d79e06f03270897d825447bc5993945 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 6 Nov 2018 15:40:29 +0100 Subject: [PATCH 081/116] Fix loglevel tests for Explore loggging --- .../app/plugins/datasource/logging/result_transformer.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/logging/result_transformer.test.ts b/public/app/plugins/datasource/logging/result_transformer.test.ts index 28debe41585..996edc56261 100644 --- a/public/app/plugins/datasource/logging/result_transformer.test.ts +++ b/public/app/plugins/datasource/logging/result_transformer.test.ts @@ -4,11 +4,11 @@ import { findCommonLabels, findUncommonLabels, formatLabels, getLogLevel, parseL describe('getLoglevel()', () => { it('returns no log level on empty line', () => { - expect(getLogLevel('')).toBe(undefined); + expect(getLogLevel('')).toBe(LogLevel.none); }); it('returns no log level on when level is part of a word', () => { - expect(getLogLevel('this is a warning')).toBe(undefined); + expect(getLogLevel('this is a warning')).toBe(LogLevel.none); }); it('returns log level on line contains a log level', () => { From 6c0c1254fe65e098bec784cc74508845d61648fa Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Sat, 3 Nov 2018 23:36:40 +0100 Subject: [PATCH 082/116] wip: panel-header: More merge conflicts --- .../dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../features/dashboard/dashgrid/DataPanel.tsx | 26 ++++--- .../dashboard/dashgrid/PanelChrome.tsx | 65 +++++++++++++--- .../dashgrid/PanelHeader/PanelHeader.tsx | 15 ++-- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 21 +++-- .../features/dashboard/utils/panel_menu.ts | 16 ++-- public/app/plugins/panel/graph2/module.tsx | 3 + .../app/plugins/panel/graph2/moduleMenu.tsx | 76 +++++++++++++++++++ public/app/types/plugins.ts | 1 + 9 files changed, 174 insertions(+), 51 deletions(-) create mode 100644 public/app/plugins/panel/graph2/moduleMenu.tsx diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index cf41595ce8c..d75e3abc67f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -115,7 +115,6 @@ export class DashboardPanel extends PureComponent { const { pluginExports } = this.state; const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper'; const panelWrapperClass = this.props.panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper'; - // this might look strange with these classes that change when edit, but // I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel return ( @@ -126,6 +125,7 @@ export class DashboardPanel extends PureComponent { withMenuOptions={pluginExports.withMenuOptions} panel={this.props.panel} dashboard={this.props.dashboard} + moduleMenu={pluginExports.moduleMenu} />
    {this.props.panel.isEditing && ( diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index a42d392c018..77460d9dc83 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,11 +1,9 @@ // Library import React, { Component } from 'react'; -// Services -import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; - // Types import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types'; +import { DataSourceApi } from 'app/types/series'; interface RenderProps { loading: LoadingState; @@ -13,7 +11,7 @@ interface RenderProps { } export interface Props { - datasource: string | null; + dataSourceApi: DataSourceApi; queries: any[]; panelId?: number; dashboardId?: number; @@ -21,6 +19,7 @@ export interface Props { timeRange?: TimeRange; refreshCounter: number; children: (r: RenderProps) => JSX.Element; + onIssueQueryResponse: any; } export interface State { @@ -60,13 +59,19 @@ export class DataPanel extends Component { } hasPropsChanged(prevProps: Props) { - return this.props.refreshCounter !== prevProps.refreshCounter || this.props.isVisible !== prevProps.isVisible; + const { refreshCounter, isVisible, dataSourceApi } = this.props; + + return ( + refreshCounter !== prevProps.refreshCounter || + isVisible !== prevProps.isVisible || + dataSourceApi !== prevProps.dataSourceApi + ); } issueQueries = async () => { - const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props; + const { isVisible, queries, panelId, dashboardId, timeRange, dataSourceApi } = this.props; - if (!isVisible) { + if (!isVisible || !dataSourceApi) { return; } @@ -78,9 +83,6 @@ export class DataPanel extends Component { this.setState({ loading: LoadingState.Loading }); try { - const dataSourceSrv = getDatasourceSrv(); - const ds = await dataSourceSrv.get(datasource); - const queryOptions: DataQueryOptions = { timezone: 'browser', panelId: panelId, @@ -96,7 +98,7 @@ export class DataPanel extends Component { }; console.log('Issuing DataPanel query', queryOptions); - const resp = await ds.query(queryOptions); + const resp = await dataSourceApi.query(queryOptions); console.log('Issuing DataPanel query Resp', resp); this.setState({ @@ -104,6 +106,8 @@ export class DataPanel extends Component { response: resp, isFirstLoad: false, }); + + this.props.onIssueQueryResponse(resp.data); } catch (err) { console.log('Loading error', err); this.setState({ loading: LoadingState.Error, isFirstLoad: false }); diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 1a6f5a3cee2..4ac0723e4e8 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -3,43 +3,62 @@ import React, { ComponentClass, PureComponent } from 'react'; // Services import { getTimeSrv } from '../time_srv'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Components import { PanelHeader } from './PanelHeader/PanelHeader'; import { DataPanel } from './DataPanel'; +import { PanelHeaderMenu } from './PanelHeader/PanelHeaderMenu'; // Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { TimeRange, PanelProps } from 'app/types'; +import { TimeRange, PanelProps, TimeSeries } from 'app/types'; +import { DataSourceApi } from 'app/types/series'; export interface PanelChromeProps { panel: PanelModel; dashboard: DashboardModel; component: ComponentClass; - withMenuOptions: any; + withMenuOptions?: (c: typeof PanelHeaderMenu, p: PanelModel) => typeof PanelHeaderMenu; + moduleMenu?: any; } export interface PanelChromeState { refreshCounter: number; renderCounter: number; timeRange?: TimeRange; + timeSeries?: TimeSeries[]; + dataSourceApi?: DataSourceApi; } export class PanelChrome extends PureComponent { constructor(props) { super(props); - this.state = { refreshCounter: 0, renderCounter: 0, }; } - componentDidMount() { + async componentDidMount() { + const { panel } = this.props; + const { datasource } = panel; + this.props.panel.events.on('refresh', this.onRefresh); this.props.panel.events.on('render', this.onRender); this.props.dashboard.panelInitialized(this.props.panel); + + try { + const dataSourceSrv = getDatasourceSrv(); + const dataSourceApi = await dataSourceSrv.get(datasource); + this.setState(prevState => ({ + ...prevState, + dataSourceApi, + })); + } catch (err) { + console.log('Datasource loading error', err); + } } componentWillUnmount() { @@ -50,10 +69,11 @@ export class PanelChrome extends PureComponent ({ + ...prevState, refreshCounter: this.state.refreshCounter + 1, timeRange: timeRange, - }); + })); }; onRender = () => { @@ -63,27 +83,50 @@ export class PanelChrome extends PureComponent { + this.setState(prevState => ({ + ...prevState, + timeSeries, + })); + }; + get isVisible() { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } render() { - const { panel, dashboard, withMenuOptions } = this.props; - const { datasource, targets } = panel; - const { timeRange, renderCounter, refreshCounter } = this.state; + const { panel, dashboard, moduleMenu } = this.props; + const { refreshCounter, timeRange, dataSourceApi, timeSeries, renderCounter } = this.state; + const { targets } = panel; const PanelComponent = this.props.component; console.log('Panel chrome render'); + // const PanelHeaderMenuComponent: typeof PanelHeaderMenu = withMenuOptions ? withMenuOptions(PanelHeaderMenu, panel) : PanelHeaderMenu; + const PanelHeaderMenuComponent = PanelHeaderMenu; + const mm = moduleMenu(panel, dataSourceApi, timeSeries); + const additionalMenuItems = mm.getAdditionalMenuItems || undefined; + const additionalSubMenuItems = mm.getAdditionalSubMenuItems || undefined; + console.log('panelChrome render'); return (
    - + + +
    {({ loading, timeSeries }) => { console.log('panelcrome inner render'); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 3d23949afd0..ba5511014f2 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -1,21 +1,16 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; -import { PanelModel } from 'app/features/dashboard/panel_model'; -import { DashboardModel } from 'app/features/dashboard/dashboard_model'; -import { PanelHeaderMenu } from './PanelHeaderMenu'; interface PanelHeaderProps { - panel: PanelModel; - dashboard: DashboardModel; - withMenuOptions: any; + title: string; } + export class PanelHeader extends PureComponent { render() { - const { dashboard, withMenuOptions, panel } = this.props; const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - const PanelHeaderMenuComponent = withMenuOptions ? withMenuOptions(PanelHeaderMenu, panel) : PanelHeaderMenu; + const { title } = this.props; return (
    @@ -34,10 +29,10 @@ export class PanelHeader extends PureComponent {
    - {this.props.panel.title} + {title} - + {this.props.children} 4m diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index c36eb9d8584..dae9e33b996 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,28 +1,25 @@ import React, { PureComponent } from 'react'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemProps } from './PanelHeaderMenuItem'; import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; +import { DataSourceApi } from 'app/types/series'; +import { TimeSeries } from 'app/types'; export interface PanelHeaderMenuProps { - panelId: number; + panel: PanelModel; dashboard: DashboardModel; - datasource: any; + dataSourceApi: DataSourceApi; additionalMenuItems?: PanelHeaderMenuItemProps[]; additionalSubMenuItems?: PanelHeaderMenuItemProps[]; + timeSeries?: TimeSeries[]; } export class PanelHeaderMenu extends PureComponent { - getPanel = () => { - // Pass in panel as prop instead? - const { panelId, dashboard } = this.props; - const panelInfo = dashboard.getPanelInfoById(panelId); - return panelInfo.panel; - }; - renderItems = (menu: PanelHeaderMenuItemProps[], isSubMenu = false) => { return (
      - {menu.map((menuItem, idx) => { + {menu.map((menuItem, idx: number) => { return ( { render() { console.log('PanelHeaderMenu render'); - const { dashboard, additionalMenuItems, additionalSubMenuItems } = this.props; - const menu = getPanelMenu(dashboard, this.getPanel(), additionalMenuItems, additionalSubMenuItems); + const { dashboard, additionalMenuItems, additionalSubMenuItems, panel } = this.props; + const menu = getPanelMenu(dashboard, panel, additionalMenuItems, additionalSubMenuItems); return
      {this.renderItems(menu)}
      ; } } diff --git a/public/app/features/dashboard/utils/panel_menu.ts b/public/app/features/dashboard/utils/panel_menu.ts index 67adf118edd..9d5fec4c6a4 100644 --- a/public/app/features/dashboard/utils/panel_menu.ts +++ b/public/app/features/dashboard/utils/panel_menu.ts @@ -80,9 +80,11 @@ export const getPanelMenu = ( handleClick: onEditPanelJson, }); - additionalSubMenuItems.forEach(item => { - menu.push(item); - }); + if (additionalSubMenuItems) { + additionalSubMenuItems.forEach(item => { + menu.push(item); + }); + } return menu; }; @@ -115,9 +117,11 @@ export const getPanelMenu = ( shortcut: 'p s', }); - additionalMenuItems.forEach(item => { - menu.push(item); - }); + if (additionalMenuItems) { + additionalMenuItems.forEach(item => { + menu.push(item); + }); + } const subMenu: PanelHeaderMenuItemProps[] = getSubMenu(); diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 88b679e1645..c0a4fef8cfc 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -7,6 +7,8 @@ import { Switch } from 'app/core/components/Switch/Switch'; import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; import { PanelProps, PanelOptionsProps, NullValueMode } from 'app/types'; +// import { moduleMenu } from './moduleMenu'; + interface Options { showBars: boolean; showLines: boolean; @@ -74,3 +76,4 @@ export class GraphOptions extends PureComponent> { export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent }; export { withMenuOptions } from './withMenuOptions'; +export { moduleMenu } from './moduleMenu'; diff --git a/public/app/plugins/panel/graph2/moduleMenu.tsx b/public/app/plugins/panel/graph2/moduleMenu.tsx new file mode 100644 index 00000000000..64729e953a8 --- /dev/null +++ b/public/app/plugins/panel/graph2/moduleMenu.tsx @@ -0,0 +1,76 @@ +import config from 'app/core/config'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getExploreUrl } from 'app/core/utils/explore'; +import { updateLocation } from 'app/core/actions'; +import { getTimeSrv } from 'app/features/dashboard/time_srv'; +import { store } from 'app/store/configureStore'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import appEvents from 'app/core/app_events'; + +import { + PanelHeaderMenuItemProps, + PanelHeaderMenuItemTypes, +} from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; + +export const moduleMenu = (panel, dataSourceApi, timeSeries) => { + const onExploreClick = async () => { + const datasourceSrv = getDatasourceSrv(); + const timeSrv = getTimeSrv(); + const url = await getExploreUrl(panel, panel.targets, dataSourceApi, datasourceSrv, timeSrv); + if (url) { + store.dispatch(updateLocation({ path: url })); + } + }; + + const onExportCsv = () => { + const model = {} as { seriesList: string }; + model.seriesList = timeSeries; + appEvents.emit('show-modal', { + templateHtml: '', + model, + modalClass: 'modal--narrow', + }); + }; + + const getAdditionalMenuItems = () => { + const items = []; + if ( + config.exploreEnabled && + contextSrv.isEditor && + dataSourceApi && + (dataSourceApi.meta.explore || dataSourceApi.meta.id === 'mixed') + ) { + items.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Explore', + handleClick: onExploreClick, + iconClassName: 'fa fa-fw fa-rocket', + shortcut: 'x', + }); + } + return items; + }; + + const getAdditionalSubMenuItems = () => { + return [ + { + type: PanelHeaderMenuItemTypes.Link, + text: 'Hello Sub Menu', + handleClick: () => { + alert('Hello world from moduleMenu'); + }, + shortcut: 'hi', + }, + { + type: PanelHeaderMenuItemTypes.Link, + text: 'Export CSV', + handleClick: onExportCsv, + }, + ] as PanelHeaderMenuItemProps[]; + }; + + return { + getAdditionalMenuItems: getAdditionalMenuItems(), + getAdditionalSubMenuItems: getAdditionalSubMenuItems(), + }; +}; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 9ede3dd9f4b..e87eef38c04 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -14,6 +14,7 @@ export interface PluginExports { PanelComponent?: ComponentClass; PanelOptionsComponent: ComponentClass; withMenuOptions?: any; + moduleMenu?: any; } export interface PanelPlugin { From 044505a2130dbe942de68d322871b02b36b7e16a Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:53:49 +0100 Subject: [PATCH 083/116] minor change Co-Authored-By: marefr --- docs/sources/enterprise/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 6d5f77ab6c8..8d44f731b3c 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -15,7 +15,7 @@ Grafana Enterprise is a commercial edition of Grafana that includes additional f version. Building on everything you already know and love about Grafana, Grafana Enterprise layers on premium data sources. -advanced authentication options, more permissions controls and 24x7x365 support and training from the core Grafana team. +advanced authentication options, more permission controls, 24x7x365 support, and training from the core Grafana team. Grafana Enterprise includes all of the features found in the open source edition. Below we list the additional features that can only be found in the Grafana Enterprise. From 881c73fb9399077805e5541df405277b3aa78b71 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:54:25 +0100 Subject: [PATCH 084/116] Update docs/sources/enterprise/index.md Co-Authored-By: marefr --- docs/sources/enterprise/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 8d44f731b3c..862c2ddb9a4 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -14,7 +14,7 @@ weight = 5 Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source version. -Building on everything you already know and love about Grafana, Grafana Enterprise layers on premium data sources. +Building on everything you already know and love about Grafana, Grafana Enterprise adds premium data sources, advanced authentication options, more permission controls, 24x7x365 support, and training from the core Grafana team. Grafana Enterprise includes all of the features found in the open source edition. Below we list the additional features From 32e001dba489415be71373f62119e4f9de1e299d Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:54:53 +0100 Subject: [PATCH 085/116] Update docs/sources/enterprise/index.md Co-Authored-By: marefr --- docs/sources/enterprise/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 862c2ddb9a4..f6dc9a02e12 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -22,7 +22,7 @@ that can only be found in the Grafana Enterprise. ### Enhanced LDAP Integration -With Grafana Enterprise you can setup synchronization between LDAP Groups and Teams. [Learn More]({{< relref "auth/enhanced_ldap.md" >}}). +With Grafana Enterprise you can set up synchronization between LDAP Groups and Teams. [Learn More]({{< relref "auth/enhanced_ldap.md" >}}). ### Datasource Permissions From 5cdd53c5e7f9d03d0b50927cbc8eaa7f2038ac58 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:55:05 +0100 Subject: [PATCH 086/116] Update docs/sources/enterprise/index.md Co-Authored-By: marefr --- docs/sources/enterprise/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index f6dc9a02e12..24b1a8234d7 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -26,7 +26,7 @@ With Grafana Enterprise you can set up synchronization between LDAP Groups and T ### Datasource Permissions -Datasource permissions allows you to restrict query access to only specific Teams and Users. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). +Datasource permissions allow you to restrict query access to only specific Teams and Users. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). ### Premium Plugins From 803b36a0593921e3d42b51b5a58cddfcf4c34e37 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:55:28 +0100 Subject: [PATCH 087/116] Update docs/sources/enterprise/index.md Co-Authored-By: marefr --- docs/sources/enterprise/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 24b1a8234d7..402e7253339 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -30,7 +30,7 @@ Datasource permissions allow you to restrict query access to only specific Teams ### Premium Plugins -With a Grafana Enterprise licence you will get access to these premium plugins. +With a Grafana Enterprise licence you will get access to premium plugins, including: * [Splunk](https://grafana.com/plugins/grafana-splunk-datasource) * [AppDynamics](https://grafana.com/plugins/dlopes7-appdynamics-datasource) From d44b8968d26dad33ba082416b4404b8cc1c15bfb Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:55:42 +0100 Subject: [PATCH 088/116] Update docs/sources/enterprise/index.md Co-Authored-By: marefr --- docs/sources/enterprise/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 402e7253339..cdd9ed3817c 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -45,7 +45,7 @@ the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Gr ## License file management -To download your Grafana Enterprise license login to you [Grafana.com](https://grafana.com) account and go to your **Org +To download your Grafana Enterprise license log in to your [Grafana.com](https://grafana.com) account and go to your **Org Profile**. In the side menu there is a section for Grafana Enterprise licenses. At the bottom of the license details page there is **Download Token** link that will download the *license.jwt* file containing your license. From 8a52cb7714e14ff19ab492fc657282e73e61c2ab Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:55:53 +0100 Subject: [PATCH 089/116] Update docs/sources/enterprise/index.md Co-Authored-By: marefr --- docs/sources/enterprise/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index cdd9ed3817c..fba31641d8b 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -58,7 +58,7 @@ You can also configure a custom location for the license file via the ini settin license_path = /company/secrets/license.jwt ``` -This setting can also be set via ENV variable. Which is useful if your running Grafana via docker and have a custom +This setting can also be set via ENV variable which is useful if you're running Grafana via docker and have a custom volume where you have placed the license file. In this case set the ENV variable `GF_ENTERPRISE_LICENSE_PATH` to point to the location of your license file. From 1bc3f0af07ae2ca7f85f056715d519d0fc40bc73 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:56:12 +0100 Subject: [PATCH 090/116] Update docs/sources/http_api/datasource_permissions.md Co-Authored-By: marefr --- docs/sources/http_api/datasource_permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/datasource_permissions.md b/docs/sources/http_api/datasource_permissions.md index bc193113b43..226beac3728 100644 --- a/docs/sources/http_api/datasource_permissions.md +++ b/docs/sources/http_api/datasource_permissions.md @@ -25,7 +25,7 @@ The permission levels for the permission field: `POST /api/datasources/:id/enable-permissions` -Enables permissions for the datasource with the given `id`. No one except Org Admins will be able to query the datasource until a permission have been added which permits certain users or teams to query the datasource. +Enables permissions for the datasource with the given `id`. No one except Org Admins will be able to query the datasource until permissions have been added which permit certain users or teams to query the datasource. **Example request**: From 4ef770fe9881f2b544d383efdb1dd57bfb9be184 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:56:32 +0100 Subject: [PATCH 091/116] Update docs/sources/permissions/dashboard_folder_permissions.md Co-Authored-By: marefr --- docs/sources/permissions/dashboard_folder_permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/permissions/dashboard_folder_permissions.md b/docs/sources/permissions/dashboard_folder_permissions.md index aed0f91ee7c..b11782b7474 100644 --- a/docs/sources/permissions/dashboard_folder_permissions.md +++ b/docs/sources/permissions/dashboard_folder_permissions.md @@ -15,7 +15,7 @@ weight = 3 {{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} For dashboards and dashboard folders there is a **Permissions** page that make it possible to -remove the default role based permissions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. +remove the default role based permissions for Editors and Viewers. On this page you can add and assign permissions to specific **Users** and **Teams**. You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. From 850c0e7111c5fb97fb80e6e7bf0eac32f69a24be Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:56:53 +0100 Subject: [PATCH 092/116] Update docs/sources/permissions/datasource_permissions.md Co-Authored-By: marefr --- docs/sources/permissions/datasource_permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md index f94fc47c4d2..6c5a98bdda5 100644 --- a/docs/sources/permissions/datasource_permissions.md +++ b/docs/sources/permissions/datasource_permissions.md @@ -15,7 +15,7 @@ weight = 4 > Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). Datasource permissions allows you to restrict access for users to query a datasource. For each datasource there is -a permission page that makes it possible to enable permissions and add restrict query permissions to specific +a permission page that makes it possible to enable permissions and restrict query permissions to specific **Users** and **Teams**. ## Restricting Access - Enable Permissions From 1347ce5f756f403c24aec8cb21549d8a9e454a42 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 6 Nov 2018 15:57:14 +0100 Subject: [PATCH 093/116] Update docs/sources/permissions/datasource_permissions.md Co-Authored-By: marefr --- docs/sources/permissions/datasource_permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md index 6c5a98bdda5..ec54c1fbccd 100644 --- a/docs/sources/permissions/datasource_permissions.md +++ b/docs/sources/permissions/datasource_permissions.md @@ -58,7 +58,7 @@ permissions to users and teams which will allow access to query the datasource. {{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_disable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_disable.gif" >}} -If you have enabled permissions for a datasource and want to revoke datasource permissions to the default, i.e. +If you have enabled permissions for a datasource and want to return datasource permissions to the default, i.e. datasource can be queried by any user in that organization, you can disable permissions with a click of a button. Note that all existing permissions created for datasource will be deleted. From dbf7f3fb6115bcd67ee5d41ed49ee7d7114e26cb Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 6 Nov 2018 09:56:27 +0100 Subject: [PATCH 094/116] wip: panel-header: More merge conflicts during cherry pick --- .../dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../dashboard/dashgrid/PanelChrome.tsx | 13 +-- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 3 +- .../PanelHeader/PanelHeaderMenuItem.tsx | 19 +--- .../features/dashboard/utils/panel_menu.ts | 2 +- public/app/plugins/panel/graph2/module.tsx | 1 - .../app/plugins/panel/graph2/moduleMenu.tsx | 10 +- .../plugins/panel/graph2/withMenuOptions.tsx | 94 ------------------- public/app/types/panel.ts | 23 +++++ public/app/types/plugins.ts | 1 - 10 files changed, 36 insertions(+), 132 deletions(-) delete mode 100644 public/app/plugins/panel/graph2/withMenuOptions.tsx diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index d75e3abc67f..9712cb75bd0 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -122,7 +122,7 @@ export class DashboardPanel extends PureComponent {
      ; - withMenuOptions?: (c: typeof PanelHeaderMenu, p: PanelModel) => typeof PanelHeaderMenu; + // withMenuOptions?: (c: typeof PanelHeaderMenu, p: PanelModel) => typeof PanelHeaderMenu; moduleMenu?: any; } @@ -99,18 +99,15 @@ export class PanelChrome extends PureComponent - void; - shortcut?: string; - children?: any; - subMenu?: PanelHeaderMenuItemProps[]; - role?: string; -} +import { PanelHeaderMenuItemProps, PanelHeaderMenuItemTypes } from 'app/types/panel'; export const PanelHeaderMenuItem: SFC = props => { const isSubMenu = props.type === PanelHeaderMenuItemTypes.SubMenu; diff --git a/public/app/features/dashboard/utils/panel_menu.ts b/public/app/features/dashboard/utils/panel_menu.ts index 9d5fec4c6a4..7e0466407f3 100644 --- a/public/app/features/dashboard/utils/panel_menu.ts +++ b/public/app/features/dashboard/utils/panel_menu.ts @@ -1,4 +1,4 @@ -import { PanelHeaderMenuItemTypes, PanelHeaderMenuItemProps } from './../dashgrid/PanelHeader/PanelHeaderMenuItem'; +import { PanelHeaderMenuItemTypes, PanelHeaderMenuItemProps } from 'app/types/panel'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; import { PanelModel } from 'app/features/dashboard/panel_model'; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index c0a4fef8cfc..d1d607afa94 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -75,5 +75,4 @@ export class GraphOptions extends PureComponent> { } export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent }; -export { withMenuOptions } from './withMenuOptions'; export { moduleMenu } from './moduleMenu'; diff --git a/public/app/plugins/panel/graph2/moduleMenu.tsx b/public/app/plugins/panel/graph2/moduleMenu.tsx index 64729e953a8..da08cbf5d9c 100644 --- a/public/app/plugins/panel/graph2/moduleMenu.tsx +++ b/public/app/plugins/panel/graph2/moduleMenu.tsx @@ -6,11 +6,7 @@ import { getTimeSrv } from 'app/features/dashboard/time_srv'; import { store } from 'app/store/configureStore'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import appEvents from 'app/core/app_events'; - -import { - PanelHeaderMenuItemProps, - PanelHeaderMenuItemTypes, -} from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; +import { PanelHeaderMenuItemProps, PanelHeaderMenuItemTypes } from 'app/types/panel'; export const moduleMenu = (panel, dataSourceApi, timeSeries) => { const onExploreClick = async () => { @@ -70,7 +66,7 @@ export const moduleMenu = (panel, dataSourceApi, timeSeries) => { }; return { - getAdditionalMenuItems: getAdditionalMenuItems(), - getAdditionalSubMenuItems: getAdditionalSubMenuItems(), + additionalMenuItems: getAdditionalMenuItems(), + additionalSubMenuItems: getAdditionalSubMenuItems(), }; }; diff --git a/public/app/plugins/panel/graph2/withMenuOptions.tsx b/public/app/plugins/panel/graph2/withMenuOptions.tsx deleted file mode 100644 index aaa89bf3406..00000000000 --- a/public/app/plugins/panel/graph2/withMenuOptions.tsx +++ /dev/null @@ -1,94 +0,0 @@ -// Libraries -import React, { PureComponent } from 'react'; - -// Services -import { getTimeSrv } from 'app/features/dashboard/time_srv'; -import { contextSrv } from 'app/core/services/context_srv'; -import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { store } from 'app/store/configureStore'; - -// Components -import { PanelHeaderMenu } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu'; -import config from 'app/core/config'; -import { getExploreUrl } from 'app/core/utils/explore'; -import { updateLocation } from 'app/core/actions'; - -// Types -import { PanelModel } from 'app/features/dashboard/panel_model'; -import { PanelHeaderMenuProps } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu'; -import { - PanelHeaderMenuItemProps, - PanelHeaderMenuItemTypes, -} from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; - -interface LocalState { - datasource: any; -} - -export const withMenuOptions = (WrappedPanelHeaderMenu: typeof PanelHeaderMenu, panel: PanelModel) => { - return class extends PureComponent { - private datasourceSrv = getDatasourceSrv(); - private timeSrv = getTimeSrv(); - - constructor(props) { - super(props); - this.state = { - datasource: undefined, - }; - } - - componentDidMount() { - const dsPromise = getDatasourceSrv().get(panel.datasource); - dsPromise.then((datasource: any) => { - this.setState(() => ({ datasource })); - }); - } - - onExploreClick = async () => { - const { datasource } = this.state; - const url = await getExploreUrl(panel, panel.targets, datasource, this.datasourceSrv, this.timeSrv); - if (url) { - store.dispatch(updateLocation({ path: url })); - } - }; - - getAdditionalMenuItems = () => { - const { datasource } = this.state; - const items = []; - if ( - config.exploreEnabled && - contextSrv.isEditor && - datasource && - (datasource.meta.explore || datasource.meta.id === 'mixed') - ) { - items.push({ - type: PanelHeaderMenuItemTypes.Link, - text: 'Explore', - handleClick: this.onExploreClick, - iconClassName: 'fa fa-fw fa-rocket', - shortcut: 'x', - }); - } - return items; - }; - - getAdditionalSubMenuItems = () => { - return [ - { - type: PanelHeaderMenuItemTypes.Link, - text: 'Hello Sub Menu', - handleClick: () => { - alert('Hello world from HOC!'); - }, - shortcut: 's h w', - }, - ] as PanelHeaderMenuItemProps[]; - }; - - render() { - const menu: PanelHeaderMenuItemProps[] = this.getAdditionalMenuItems(); - const subMenu: PanelHeaderMenuItemProps[] = this.getAdditionalSubMenuItems(); - return ; - } - }; -}; diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index 7febd0cad26..8b12caffe1a 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -12,3 +12,26 @@ export interface PanelOptionsProps { options: T; onChange: (options: T) => void; } + +export enum PanelHeaderMenuItemTypes { // TODO: Evaluate. Remove? + Button = 'Button', // ? + Divider = 'Divider', + Link = 'Link', + SubMenu = 'SubMenu', +} + +export interface PanelHeaderMenuItemProps { + type: PanelHeaderMenuItemTypes; + text?: string; + iconClassName?: string; + handleClick?: () => void; + shortcut?: string; + children?: any; + subMenu?: PanelHeaderMenuItemProps[]; + role?: string; +} + +export interface PanelMenuExtras { + additionalMenuItems: PanelHeaderMenuItemProps[]; + additionalSubMenuItems: PanelHeaderMenuItemProps[]; +} diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index e87eef38c04..0f4b2928595 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -13,7 +13,6 @@ export interface PluginExports { PanelCtrl?; PanelComponent?: ComponentClass; PanelOptionsComponent: ComponentClass; - withMenuOptions?: any; moduleMenu?: any; } From 49550ccedfd1d5b051c7384cd9724f2259cb80be Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 6 Nov 2018 15:03:56 +0100 Subject: [PATCH 095/116] wip: panel-header: More merge conflicts during cherry pick --- .../features/dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../app/features/dashboard/dashgrid/PanelChrome.tsx | 11 ++++++----- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 2 +- public/app/plugins/panel/graph2/module.tsx | 4 +--- public/app/plugins/panel/graph2/moduleMenu.tsx | 7 +++++-- public/app/types/panel.ts | 9 +++++++-- public/app/types/plugins.ts | 3 ++- 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 9712cb75bd0..808c861f269 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -125,7 +125,7 @@ export class DashboardPanel extends PureComponent { // withMenuOptions={pluginExports.withMenuOptions} panel={this.props.panel} dashboard={this.props.dashboard} - moduleMenu={pluginExports.moduleMenu} + getMenuAdditional={pluginExports.getMenuAdditional} />
      {this.props.panel.isEditing && ( diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index e4f5523680b..ba0e9ecedce 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -14,14 +14,14 @@ import { PanelHeaderMenu } from './PanelHeader/PanelHeaderMenu'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { TimeRange, PanelProps, TimeSeries } from 'app/types'; +import { PanelHeaderGetMenuAdditional } from 'app/types/panel'; import { DataSourceApi } from 'app/types/series'; export interface PanelChromeProps { panel: PanelModel; dashboard: DashboardModel; component: ComponentClass; - // withMenuOptions?: (c: typeof PanelHeaderMenu, p: PanelModel) => typeof PanelHeaderMenu; - moduleMenu?: any; + getMenuAdditional?: PanelHeaderGetMenuAdditional; } export interface PanelChromeState { @@ -52,7 +52,7 @@ export class PanelChrome extends PureComponent ({ + this.setState((prevState: PanelChromeState) => ({ ...prevState, dataSourceApi, })); @@ -95,11 +95,12 @@ export class PanelChrome extends PureComponent> { } export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent }; -export { moduleMenu } from './moduleMenu'; +export { getMenuAdditional } from './moduleMenu'; diff --git a/public/app/plugins/panel/graph2/moduleMenu.tsx b/public/app/plugins/panel/graph2/moduleMenu.tsx index da08cbf5d9c..2951c9e0fe1 100644 --- a/public/app/plugins/panel/graph2/moduleMenu.tsx +++ b/public/app/plugins/panel/graph2/moduleMenu.tsx @@ -7,8 +7,11 @@ import { store } from 'app/store/configureStore'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import appEvents from 'app/core/app_events'; import { PanelHeaderMenuItemProps, PanelHeaderMenuItemTypes } from 'app/types/panel'; +import { TimeSeries } from 'app/types/series'; +import { DataSource } from 'app/types/datasources'; +import { PanelModel } from 'app/features/dashboard/panel_model'; -export const moduleMenu = (panel, dataSourceApi, timeSeries) => { +export const getMenuAdditional = (panel: PanelModel, dataSourceApi: DataSource, timeSeries: TimeSeries[]) => { const onExploreClick = async () => { const datasourceSrv = getDatasourceSrv(); const timeSrv = getTimeSrv(); @@ -19,7 +22,7 @@ export const moduleMenu = (panel, dataSourceApi, timeSeries) => { }; const onExportCsv = () => { - const model = {} as { seriesList: string }; + const model = {} as { seriesList: TimeSeries[] }; model.seriesList = timeSeries; appEvents.emit('show-modal', { templateHtml: '', diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index 8b12caffe1a..815aefd6203 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -1,4 +1,5 @@ -import { LoadingState, TimeSeries, TimeRange } from './series'; +import { LoadingState, TimeSeries, TimeRange, DataSourceApi } from './series'; +import { PanelModel } from 'app/features/dashboard/panel_model'; export interface PanelProps { timeSeries: TimeSeries[]; @@ -31,7 +32,11 @@ export interface PanelHeaderMenuItemProps { role?: string; } -export interface PanelMenuExtras { +export interface PanelHeaderMenuAdditional { additionalMenuItems: PanelHeaderMenuItemProps[]; additionalSubMenuItems: PanelHeaderMenuItemProps[]; } + +export interface PanelHeaderGetMenuAdditional { + (panel: PanelModel, dataSourceApi: DataSourceApi, timeSeries: TimeSeries[]): PanelHeaderMenuAdditional; +} diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 0f4b2928595..114979641b8 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -1,5 +1,6 @@ import { ComponentClass } from 'react'; import { PanelProps, PanelOptionsProps } from './panel'; +import { PanelHeaderGetMenuAdditional } from 'app/types/panel'; export interface PluginExports { Datasource?: any; @@ -13,7 +14,7 @@ export interface PluginExports { PanelCtrl?; PanelComponent?: ComponentClass; PanelOptionsComponent: ComponentClass; - moduleMenu?: any; + getMenuAdditional?: PanelHeaderGetMenuAdditional; } export interface PanelPlugin { From dd7437e9e925c718c24a6dfde9451f07dd6a6c43 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 6 Nov 2018 16:37:51 +0100 Subject: [PATCH 096/116] wip: panel-header: Reverted a lot of code to pause the "custom menu options" for now --- .../features/dashboard/dashgrid/DataPanel.tsx | 27 ++++---- .../dashboard/dashgrid/PanelChrome.tsx | 48 ++++---------- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 4 -- .../app/plugins/panel/graph2/moduleMenu.tsx | 66 ++++--------------- public/app/types/panel.ts | 4 +- 5 files changed, 38 insertions(+), 111 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 77460d9dc83..d0122363668 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,9 +1,11 @@ // Library import React, { Component } from 'react'; +// Services +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; + // Types import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types'; -import { DataSourceApi } from 'app/types/series'; interface RenderProps { loading: LoadingState; @@ -11,7 +13,7 @@ interface RenderProps { } export interface Props { - dataSourceApi: DataSourceApi; + datasource: string | null; queries: any[]; panelId?: number; dashboardId?: number; @@ -19,7 +21,6 @@ export interface Props { timeRange?: TimeRange; refreshCounter: number; children: (r: RenderProps) => JSX.Element; - onIssueQueryResponse: any; } export interface State { @@ -37,6 +38,7 @@ export class DataPanel extends Component { constructor(props: Props) { super(props); + this.state = { loading: LoadingState.NotStarted, response: { @@ -59,19 +61,13 @@ export class DataPanel extends Component { } hasPropsChanged(prevProps: Props) { - const { refreshCounter, isVisible, dataSourceApi } = this.props; - - return ( - refreshCounter !== prevProps.refreshCounter || - isVisible !== prevProps.isVisible || - dataSourceApi !== prevProps.dataSourceApi - ); + return this.props.refreshCounter !== prevProps.refreshCounter || this.props.isVisible !== prevProps.isVisible; } issueQueries = async () => { - const { isVisible, queries, panelId, dashboardId, timeRange, dataSourceApi } = this.props; + const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props; - if (!isVisible || !dataSourceApi) { + if (!isVisible) { return; } @@ -83,6 +79,9 @@ export class DataPanel extends Component { this.setState({ loading: LoadingState.Loading }); try { + const dataSourceSrv = getDatasourceSrv(); + const ds = await dataSourceSrv.get(datasource); + const queryOptions: DataQueryOptions = { timezone: 'browser', panelId: panelId, @@ -98,7 +97,7 @@ export class DataPanel extends Component { }; console.log('Issuing DataPanel query', queryOptions); - const resp = await dataSourceApi.query(queryOptions); + const resp = await ds.query(queryOptions); console.log('Issuing DataPanel query Resp', resp); this.setState({ @@ -106,8 +105,6 @@ export class DataPanel extends Component { response: resp, isFirstLoad: false, }); - - this.props.onIssueQueryResponse(resp.data); } catch (err) { console.log('Loading error', err); this.setState({ loading: LoadingState.Error, isFirstLoad: false }); diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index ba0e9ecedce..4a4669a3950 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -3,7 +3,6 @@ import React, { ComponentClass, PureComponent } from 'react'; // Services import { getTimeSrv } from '../time_srv'; -import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Components import { PanelHeader } from './PanelHeader/PanelHeader'; @@ -13,52 +12,36 @@ import { PanelHeaderMenu } from './PanelHeader/PanelHeaderMenu'; // Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { TimeRange, PanelProps, TimeSeries } from 'app/types'; +import { TimeRange, PanelProps } from 'app/types'; import { PanelHeaderGetMenuAdditional } from 'app/types/panel'; -import { DataSourceApi } from 'app/types/series'; -export interface PanelChromeProps { +export interface Props { panel: PanelModel; dashboard: DashboardModel; component: ComponentClass; getMenuAdditional?: PanelHeaderGetMenuAdditional; } -export interface PanelChromeState { +export interface State { refreshCounter: number; renderCounter: number; timeRange?: TimeRange; - timeSeries?: TimeSeries[]; - dataSourceApi?: DataSourceApi; } -export class PanelChrome extends PureComponent { +export class PanelChrome extends PureComponent { constructor(props) { super(props); + this.state = { refreshCounter: 0, renderCounter: 0, }; } - async componentDidMount() { - const { panel } = this.props; - const { datasource } = panel; - + componentDidMount() { this.props.panel.events.on('refresh', this.onRefresh); this.props.panel.events.on('render', this.onRender); this.props.dashboard.panelInitialized(this.props.panel); - - try { - const dataSourceSrv = getDatasourceSrv(); - const dataSourceApi = await dataSourceSrv.get(datasource); - this.setState((prevState: PanelChromeState) => ({ - ...prevState, - dataSourceApi, - })); - } catch (err) { - console.log('Datasource loading error', err); - } } componentWillUnmount() { @@ -78,15 +61,9 @@ export class PanelChrome extends PureComponent { console.log('onRender'); - this.setState({ - renderCounter: this.state.renderCounter + 1, - }); - }; - - onIssueQueryResponse = (timeSeries: any) => { this.setState(prevState => ({ ...prevState, - timeSeries, + renderCounter: this.state.renderCounter + 1, })); }; @@ -96,11 +73,11 @@ export class PanelChrome extends PureComponent
      {({ loading, timeSeries }) => { console.log('panelcrome inner render'); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 80cd5663658..aa55a35743f 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -4,16 +4,12 @@ import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelHeaderMenuItem } from './PanelHeaderMenuItem'; import { PanelHeaderMenuItemProps } from 'app/types/panel'; import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; -import { TimeSeries } from 'app/types'; -import { DataSourceApi } from 'app/types/series'; export interface PanelHeaderMenuProps { panel: PanelModel; dashboard: DashboardModel; - dataSourceApi: DataSourceApi; additionalMenuItems?: PanelHeaderMenuItemProps[]; additionalSubMenuItems?: PanelHeaderMenuItemProps[]; - timeSeries?: TimeSeries[]; } export class PanelHeaderMenu extends PureComponent { diff --git a/public/app/plugins/panel/graph2/moduleMenu.tsx b/public/app/plugins/panel/graph2/moduleMenu.tsx index 2951c9e0fe1..7ceff68514f 100644 --- a/public/app/plugins/panel/graph2/moduleMenu.tsx +++ b/public/app/plugins/panel/graph2/moduleMenu.tsx @@ -1,53 +1,18 @@ -import config from 'app/core/config'; -import { contextSrv } from 'app/core/services/context_srv'; -import { getExploreUrl } from 'app/core/utils/explore'; -import { updateLocation } from 'app/core/actions'; -import { getTimeSrv } from 'app/features/dashboard/time_srv'; -import { store } from 'app/store/configureStore'; -import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import appEvents from 'app/core/app_events'; -import { PanelHeaderMenuItemProps, PanelHeaderMenuItemTypes } from 'app/types/panel'; -import { TimeSeries } from 'app/types/series'; -import { DataSource } from 'app/types/datasources'; +import { PanelHeaderMenuItemProps, PanelHeaderMenuItemTypes } from 'app/types/panel'; import { PanelModel } from 'app/features/dashboard/panel_model'; -export const getMenuAdditional = (panel: PanelModel, dataSourceApi: DataSource, timeSeries: TimeSeries[]) => { - const onExploreClick = async () => { - const datasourceSrv = getDatasourceSrv(); - const timeSrv = getTimeSrv(); - const url = await getExploreUrl(panel, panel.targets, dataSourceApi, datasourceSrv, timeSrv); - if (url) { - store.dispatch(updateLocation({ path: url })); - } - }; - - const onExportCsv = () => { - const model = {} as { seriesList: TimeSeries[] }; - model.seriesList = timeSeries; - appEvents.emit('show-modal', { - templateHtml: '', - model, - modalClass: 'modal--narrow', - }); - }; - +export const getMenuAdditional = (panel: PanelModel) => { const getAdditionalMenuItems = () => { - const items = []; - if ( - config.exploreEnabled && - contextSrv.isEditor && - dataSourceApi && - (dataSourceApi.meta.explore || dataSourceApi.meta.id === 'mixed') - ) { - items.push({ + return [ + { type: PanelHeaderMenuItemTypes.Link, - text: 'Explore', - handleClick: onExploreClick, - iconClassName: 'fa fa-fw fa-rocket', - shortcut: 'x', - }); - } - return items; + text: 'Hello menu', + handleClick: () => { + alert('Hello world from menu'); + }, + shortcut: 'hi', + }, + ] as PanelHeaderMenuItemProps[]; }; const getAdditionalSubMenuItems = () => { @@ -56,14 +21,9 @@ export const getMenuAdditional = (panel: PanelModel, dataSourceApi: DataSource, type: PanelHeaderMenuItemTypes.Link, text: 'Hello Sub Menu', handleClick: () => { - alert('Hello world from moduleMenu'); + alert('Hello world from sub menu'); }, - shortcut: 'hi', - }, - { - type: PanelHeaderMenuItemTypes.Link, - text: 'Export CSV', - handleClick: onExportCsv, + shortcut: 'subhi', }, ] as PanelHeaderMenuItemProps[]; }; diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index 815aefd6203..17a312bf55a 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -1,4 +1,4 @@ -import { LoadingState, TimeSeries, TimeRange, DataSourceApi } from './series'; +import { LoadingState, TimeSeries, TimeRange } from './series'; import { PanelModel } from 'app/features/dashboard/panel_model'; export interface PanelProps { @@ -38,5 +38,5 @@ export interface PanelHeaderMenuAdditional { } export interface PanelHeaderGetMenuAdditional { - (panel: PanelModel, dataSourceApi: DataSourceApi, timeSeries: TimeSeries[]): PanelHeaderMenuAdditional; + (panel: PanelModel): PanelHeaderMenuAdditional; } From 9c28ff8f84aebd5d8ea3ac4b078b6a404f544bc1 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 6 Nov 2018 16:44:13 +0100 Subject: [PATCH 097/116] wip: panel-header: Remove custom menu items from panels completely --- .../app/features/dashboard/dashgrid/PanelChrome.tsx | 12 ++---------- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 4a4669a3950..818dbb6d155 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -72,25 +72,17 @@ export class PanelChrome extends PureComponent { } render() { - const { panel, dashboard, getMenuAdditional } = this.props; + const { panel, dashboard } = this.props; const { refreshCounter, timeRange, renderCounter } = this.state; const { datasource, targets } = panel; const PanelComponent = this.props.component; - const panelSpecificMenuOptions = getMenuAdditional(panel); - const additionalMenuItems = panelSpecificMenuOptions.additionalMenuItems || undefined; - const additionalSubMenuItems = panelSpecificMenuOptions.additionalSubMenuItems || undefined; console.log('panelChrome render'); return (
      - +
      { {menu.map((menuItem, idx: number) => { return ( Date: Tue, 6 Nov 2018 16:55:44 +0100 Subject: [PATCH 098/116] wip: panel-header: Fix shareModal compatibility with react and angular --- public/app/features/dashboard/shareModalCtrl.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index f894d24202f..b4ce25485b8 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -12,8 +12,8 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, $scope.editor = { index: $scope.tabIndex || 0 }; $scope.init = () => { - $scope.panel = $scope.model.panel || $scope.panel; // React pass panel and dashboard in the "model" property - $scope.dashboard = $scope.model.dashboard || $scope.dashboard; + $scope.panel = $scope.model && $scope.model.panel ? $scope.model.panel : $scope.panel; // React pass panel and dashboard in the "model" property + $scope.dashboard = $scope.model && $scope.model.dashboard ? $scope.model.dashboard : $scope.dashboard; // ^ $scope.modeSharePanel = $scope.panel ? true : false; $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }]; From f294dbdb86c9abca662fd71d99c6adf01791ede5 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 6 Nov 2018 17:39:35 +0100 Subject: [PATCH 099/116] move enterprise down in menu --- docs/sources/enterprise/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index fba31641d8b..43ec2cd65c2 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -6,7 +6,7 @@ type = "docs" [menu.docs] name = "Grafana Enterprise" identifier = "enterprise" -weight = 5 +weight = 30 +++ # Grafana Enterprise From 1d3b8e25ce52cd73de185f2c6bb206332f8ed7c8 Mon Sep 17 00:00:00 2001 From: Alexandre de Verteuil Date: Tue, 6 Nov 2018 15:41:37 -0500 Subject: [PATCH 100/116] Fix typo in docs/sources/reference/scripting.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change: > In the folder grafana install folder under `public/dashboards/` there is a file named `scripted.js`. …to: > In the grafana install folder under `public/dashboards/` there is a file named `scripted.js`. --- docs/sources/reference/scripting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/scripting.md b/docs/sources/reference/scripting.md index 7f218765d39..12ab91f3c3c 100644 --- a/docs/sources/reference/scripting.md +++ b/docs/sources/reference/scripting.md @@ -12,7 +12,7 @@ weight = 9 If you have lots of metric names that change (new servers etc) in a defined pattern it is irritating to constantly have to create new dashboards. -With scripted dashboards you can dynamically create your dashboards using javascript. In the folder grafana install folder +With scripted dashboards you can dynamically create your dashboards using javascript. In the grafana install folder under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url: `http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName` From d0794dbce1d353d0c9339d45b9a757e2364cf10e Mon Sep 17 00:00:00 2001 From: Alexandre de Verteuil Date: Wed, 7 Nov 2018 09:17:36 +0100 Subject: [PATCH 101/116] Update docs/sources/permissions/dashboard_folder_permissions.md Co-Authored-By: marefr --- docs/sources/permissions/dashboard_folder_permissions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sources/permissions/dashboard_folder_permissions.md b/docs/sources/permissions/dashboard_folder_permissions.md index b11782b7474..83cb0ee86a3 100644 --- a/docs/sources/permissions/dashboard_folder_permissions.md +++ b/docs/sources/permissions/dashboard_folder_permissions.md @@ -62,6 +62,12 @@ Permissions for a dashboard: Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins. +## Summary + - **View**: Can only view existing dashboards/folders. - You cannot override permissions for users with **Org Admin Role** +- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. + +For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. +- You cannot override permissions for users with **Org Admin Role** - A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. From a24f6998f28e76702d6ef549be554b7f3b01c579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 7 Nov 2018 13:36:35 +0100 Subject: [PATCH 102/116] refactorings and some clean-up / removal of things not used --- .../dashboard/dashgrid/DashboardPanel.tsx | 4 +- .../dashboard/dashgrid/PanelChrome.tsx | 2 - .../dashgrid/PanelHeader/PanelHeader.tsx | 4 +- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 19 ++-- .../PanelHeader/PanelHeaderMenuItem.tsx | 12 +-- .../features/dashboard/utils/panel_menu.ts | 96 ++++++------------- public/app/plugins/panel/graph2/module.tsx | 1 - .../app/plugins/panel/graph2/moduleMenu.tsx | 35 ------- public/app/types/panel.ts | 26 +---- public/app/types/plugins.ts | 2 - 10 files changed, 52 insertions(+), 149 deletions(-) delete mode 100644 public/app/plugins/panel/graph2/moduleMenu.tsx diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 808c861f269..dd96c7b698e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; @@ -122,10 +122,8 @@ export class DashboardPanel extends PureComponent {
      {this.props.panel.isEditing && ( diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 818dbb6d155..ce000342b9f 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -13,13 +13,11 @@ import { PanelHeaderMenu } from './PanelHeader/PanelHeaderMenu'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { TimeRange, PanelProps } from 'app/types'; -import { PanelHeaderGetMenuAdditional } from 'app/types/panel'; export interface Props { panel: PanelModel; dashboard: DashboardModel; component: ComponentClass; - getMenuAdditional?: PanelHeaderGetMenuAdditional; } export interface State { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index ba5511014f2..ca4a6fe733c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -1,11 +1,11 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; -interface PanelHeaderProps { +interface Props { title: string; } -export class PanelHeader extends PureComponent { +export class PanelHeader extends PureComponent { render() { const isFullscreen = false; const isLoading = false; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index dfb83be9d1d..407f3c7ad1c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,19 +1,17 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelHeaderMenuItem } from './PanelHeaderMenuItem'; -import { PanelHeaderMenuItemProps } from 'app/types/panel'; import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; +import { PanelMenuItem } from 'app/types/panel'; -export interface PanelHeaderMenuProps { +export interface Props { panel: PanelModel; dashboard: DashboardModel; - additionalMenuItems?: PanelHeaderMenuItemProps[]; - additionalSubMenuItems?: PanelHeaderMenuItemProps[]; } -export class PanelHeaderMenu extends PureComponent { - renderItems = (menu: PanelHeaderMenuItemProps[], isSubMenu = false) => { +export class PanelHeaderMenu extends PureComponent { + renderItems = (menu: PanelMenuItem[], isSubMenu = false) => { return (