From b84fd3a7aea83f894bcc2dc9246507fc84aa49fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 7 Feb 2018 17:54:21 +0100 Subject: [PATCH 01/73] fix: initial fix for #10822 --- pkg/services/sqlstore/dashboard.go | 1 + pkg/services/sqlstore/search_builder.go | 22 ++---------- pkg/services/sqlstore/sqlbuilder.go | 45 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 pkg/services/sqlstore/sqlbuilder.go diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index be8b11b1f5b..c187360dc33 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -279,6 +279,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear var res []DashboardSearchProjection sql, params := sb.ToSql() + sqlog.Info("sql", "sql", sql, "params", params) err := x.Sql(sql, params...).Find(&res) if err != nil { return nil, err diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index 627074d5453..91e2742e165 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -1,7 +1,6 @@ package sqlstore import ( - "bytes" "strings" m "github.com/grafana/grafana/pkg/models" @@ -9,6 +8,7 @@ import ( // SearchBuilder is a builder/object mother that builds a dashboard search query type SearchBuilder struct { + SqlBuilder tags []string isStarred bool limit int @@ -18,8 +18,6 @@ type SearchBuilder struct { whereTypeFolder bool whereTypeDash bool whereFolderIds []int64 - sql bytes.Buffer - params []interface{} } func NewSearchBuilder(signedInUser *m.SignedInUser, limit int) *SearchBuilder { @@ -176,23 +174,7 @@ func (sb *SearchBuilder) buildSearchWhereClause() { } } - if sb.signedInUser.OrgRole != m.ROLE_ADMIN { - allowedDashboardsSubQuery := ` AND (dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR dashboard.id in ( - SELECT distinct d.id AS DashboardId - FROM dashboard AS d - LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id - LEFT JOIN team_member as ugm on ugm.team_id = da.team_id - LEFT JOIN org_user ou on ou.role = da.role - WHERE - d.has_acl = ` + dialect.BooleanStr(true) + ` and - (da.user_id = ? or ugm.user_id = ? or ou.id is not null) - and d.org_id = ? - ) - )` - - sb.sql.WriteString(allowedDashboardsSubQuery) - sb.params = append(sb.params, sb.signedInUser.UserId, sb.signedInUser.UserId, sb.signedInUser.OrgId) - } + sb.writeDashboardPermissionFilter(sb.signedInUser, m.PERMISSION_VIEW) if len(sb.whereTitle) > 0 { sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") diff --git a/pkg/services/sqlstore/sqlbuilder.go b/pkg/services/sqlstore/sqlbuilder.go new file mode 100644 index 00000000000..9a0dd0d3989 --- /dev/null +++ b/pkg/services/sqlstore/sqlbuilder.go @@ -0,0 +1,45 @@ +package sqlstore + +import ( + "bytes" + "strings" + + m "github.com/grafana/grafana/pkg/models" +) + +type SqlBuilder struct { + sql bytes.Buffer + params []interface{} +} + +func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, minPermission m.PermissionType) { + + if user.OrgRole == m.ROLE_ADMIN { + return + } + + okRoles := []interface{}{user.OrgRole} + + if user.OrgRole == m.ROLE_EDITOR { + okRoles = append(okRoles, m.ROLE_VIEWER) + } + + sb.sql.WriteString(` AND + ( + dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR + dashboard.id in ( + SELECT distinct d.id AS DashboardId + FROM dashboard AS d + LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id + LEFT JOIN team_member as ugm on ugm.team_id = da.team_id + WHERE + d.has_acl = ` + dialect.BooleanStr(true) + ` AND + d.org_id = ? AND + da.permission >= ? AND + (da.user_id = ? or ugm.user_id = ? or da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)) + ) + )`) + + sb.params = append(sb.params, user.OrgId, minPermission, user.UserId, user.UserId) + sb.params = append(sb.params, okRoles...) +} From 8e8f3c4332fa6effd3111be5bb83178e64e44def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 8 Feb 2018 17:11:01 +0100 Subject: [PATCH 02/73] dashboard and folder search with permissions --- pkg/api/api.go | 2 - pkg/api/dashboard.go | 16 ------ pkg/api/search.go | 7 +++ pkg/models/dashboards.go | 12 ----- pkg/services/search/handlers.go | 1 + pkg/services/search/models.go | 3 +- pkg/services/sqlstore/dashboard.go | 53 ++----------------- .../sqlstore/dashboard_folder_test.go | 26 +++++---- pkg/services/sqlstore/search_builder.go | 6 ++- pkg/services/sqlstore/search_builder_test.go | 3 +- pkg/services/sqlstore/sqlbuilder.go | 4 +- .../dashboard/folder_picker/folder_picker.ts | 8 ++- 12 files changed, 43 insertions(+), 98 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 752af7602f5..793f5a2e830 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -261,8 +261,6 @@ func (hs *HttpServer) registerRoutes() { dashboardRoute.Get("/tags", GetDashboardTags) dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) - dashboardRoute.Get("/folders", wrap(GetFoldersForSignedInUser)) - dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) { dashIdRoute.Get("/versions", wrap(GetDashboardVersions)) dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index d7676899eb2..a1f3560c0c6 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -490,19 +490,3 @@ func GetDashboardTags(c *middleware.Context) { c.JSON(200, query.Result) } - -func GetFoldersForSignedInUser(c *middleware.Context) Response { - title := c.Query("query") - query := m.GetFoldersForSignedInUserQuery{ - OrgId: c.OrgId, - SignedInUser: c.SignedInUser, - Title: title, - } - - err := bus.Dispatch(&query) - if err != nil { - return ApiError(500, "Failed to get folders from database", err) - } - - return Json(200, query.Result) -} diff --git a/pkg/api/search.go b/pkg/api/search.go index fee062a5599..f79385d83f8 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" ) @@ -15,11 +16,16 @@ func Search(c *middleware.Context) { starred := c.Query("starred") limit := c.QueryInt("limit") dashboardType := c.Query("type") + permission := models.PERMISSION_VIEW if limit == 0 { limit = 1000 } + if c.Query("permission") == "Edit" { + permission = models.PERMISSION_EDIT + } + dbids := make([]int64, 0) for _, id := range c.QueryStrings("dashboardIds") { dashboardId, err := strconv.ParseInt(id, 10, 64) @@ -46,6 +52,7 @@ func Search(c *middleware.Context) { DashboardIds: dbids, Type: dashboardType, FolderIds: folderIds, + Permission: permission, } err := bus.Dispatch(&searchQuery) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 12216718b44..a91d4c4ed62 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -270,18 +270,6 @@ type GetDashboardsBySlugQuery struct { Result []*Dashboard } -type GetFoldersForSignedInUserQuery struct { - OrgId int64 - SignedInUser *SignedInUser - Title string - Result []*DashboardFolder -} - -type DashboardFolder struct { - Id int64 `json:"id"` - Title string `json:"title"` -} - type DashboardPermissionForUser struct { DashboardId int64 `json:"dashboardId"` Permission PermissionType `json:"permission"` diff --git a/pkg/services/search/handlers.go b/pkg/services/search/handlers.go index 247585402ef..cf194c320bb 100644 --- a/pkg/services/search/handlers.go +++ b/pkg/services/search/handlers.go @@ -21,6 +21,7 @@ func searchHandler(query *Query) error { FolderIds: query.FolderIds, Tags: query.Tags, Limit: query.Limit, + Permission: query.Permission, } if err := bus.Dispatch(&dashQuery); err != nil { diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index 6dea975d9fe..2da09672f13 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -52,6 +52,7 @@ type Query struct { Type string DashboardIds []int64 FolderIds []int64 + Permission models.PermissionType Result HitList } @@ -66,7 +67,7 @@ type FindPersistedDashboardsQuery struct { FolderIds []int64 Tags []string Limit int - IsBrowse bool + Permission models.PermissionType Result HitList } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index c187360dc33..dd739343d7b 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -1,6 +1,7 @@ package sqlstore import ( + "fmt" "strings" "time" @@ -21,7 +22,6 @@ func init() { bus.AddHandler("sql", GetDashboardSlugById) bus.AddHandler("sql", GetDashboardUIDById) bus.AddHandler("sql", GetDashboardsByPluginId) - bus.AddHandler("sql", GetFoldersForSignedInUser) bus.AddHandler("sql", GetDashboardPermissionsForUser) bus.AddHandler("sql", GetDashboardsBySlug) } @@ -256,7 +256,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear limit = 1000 } - sb := NewSearchBuilder(query.SignedInUser, limit). + sb := NewSearchBuilder(query.SignedInUser, limit, query.Permission). WithTags(query.Tags). WithDashboardIdsIn(query.DashboardIds) @@ -279,6 +279,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear var res []DashboardSearchProjection sql, params := sb.ToSql() + fmt.Printf("%s, %v", sql, params) sqlog.Info("sql", "sql", sql, "params", params) err := x.Sql(sql, params...).Find(&res) if err != nil { @@ -358,54 +359,6 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { return err } -func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { - query.Result = make([]*m.DashboardFolder, 0) - var err error - - if query.SignedInUser.OrgRole == m.ROLE_ADMIN { - sql := `SELECT distinct d.id, d.title - FROM dashboard AS d WHERE d.is_folder = ? AND d.org_id = ? - ORDER BY d.title ASC` - - err = x.Sql(sql, dialect.BooleanStr(true), query.OrgId).Find(&query.Result) - } else { - params := make([]interface{}, 0) - sql := `SELECT distinct d.id, d.title - FROM dashboard AS d - LEFT JOIN dashboard_acl AS da ON d.id = da.dashboard_id - LEFT JOIN team_member AS ugm ON ugm.team_id = da.team_id - LEFT JOIN org_user ou ON ou.role = da.role AND ou.user_id = ? - LEFT JOIN org_user ouRole ON ouRole.role = 'Editor' AND ouRole.user_id = ? AND ouRole.org_id = ?` - params = append(params, query.SignedInUser.UserId) - params = append(params, query.SignedInUser.UserId) - params = append(params, query.OrgId) - - sql += ` WHERE - d.org_id = ? AND - d.is_folder = ? AND - ( - (d.has_acl = ? AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL)) - OR (d.has_acl = ? AND ouRole.id IS NOT NULL) - )` - params = append(params, query.OrgId) - params = append(params, dialect.BooleanStr(true)) - params = append(params, dialect.BooleanStr(true)) - params = append(params, query.SignedInUser.UserId) - params = append(params, query.SignedInUser.UserId) - params = append(params, dialect.BooleanStr(false)) - - if len(query.Title) > 0 { - sql += " AND d.title " + dialect.LikeStr() + " ?" - params = append(params, "%"+query.Title+"%") - } - - sql += ` ORDER BY d.title ASC` - err = x.Sql(sql, params...).Find(&query.Result) - } - - return err -} - func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { return inTransaction(func(sess *DBSession) error { dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId} diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index 4818deaae14..bd09e7490cb 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -227,12 +227,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("Admin users", func() { Convey("Should have write access to all dashboard folders in their org", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := search.FindPersistedDashboardsQuery{ OrgId: 1, - SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN}, + SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN, OrgId: 1}, + Permission: m.PERMISSION_VIEW, + Type: "dash-folder", } - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -260,13 +262,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Editor users", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := search.FindPersistedDashboardsQuery{ OrgId: 1, - SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR}, + SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR, OrgId: 1}, + Permission: m.PERMISSION_EDIT, } Convey("Should have write access to all dashboard folders with default ACL", func() { - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -295,7 +298,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() { updateTestDashboardWithAcl(folder1.Id, editorUser.Id, m.PERMISSION_VIEW) - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) @@ -305,13 +308,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Viewer users", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := search.FindPersistedDashboardsQuery{ OrgId: 1, - SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER}, + SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER, OrgId: 1}, + Permission: m.PERMISSION_EDIT, } Convey("Should have no write access to any dashboard folders with default ACL", func() { - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 0) @@ -338,7 +342,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() { updateTestDashboardWithAcl(folder1.Id, viewerUser.Id, m.PERMISSION_EDIT) - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index 91e2742e165..0db0fca53b0 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -18,12 +18,14 @@ type SearchBuilder struct { whereTypeFolder bool whereTypeDash bool whereFolderIds []int64 + permission m.PermissionType } -func NewSearchBuilder(signedInUser *m.SignedInUser, limit int) *SearchBuilder { +func NewSearchBuilder(signedInUser *m.SignedInUser, limit int, permission m.PermissionType) *SearchBuilder { searchBuilder := &SearchBuilder{ signedInUser: signedInUser, limit: limit, + permission: permission, } return searchBuilder @@ -174,7 +176,7 @@ func (sb *SearchBuilder) buildSearchWhereClause() { } } - sb.writeDashboardPermissionFilter(sb.signedInUser, m.PERMISSION_VIEW) + sb.writeDashboardPermissionFilter(sb.signedInUser, sb.permission) if len(sb.whereTitle) > 0 { sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") diff --git a/pkg/services/sqlstore/search_builder_test.go b/pkg/services/sqlstore/search_builder_test.go index 32ccbc583f5..e8b02c445ec 100644 --- a/pkg/services/sqlstore/search_builder_test.go +++ b/pkg/services/sqlstore/search_builder_test.go @@ -16,7 +16,8 @@ func TestSearchBuilder(t *testing.T) { OrgId: 1, UserId: 1, } - sb := NewSearchBuilder(signedInUser, 1000) + + sb := NewSearchBuilder(signedInUser, 1000, m.PERMISSION_VIEW) Convey("When building a normal search", func() { sql, params := sb.IsStarred().WithTitle("test").ToSql() diff --git a/pkg/services/sqlstore/sqlbuilder.go b/pkg/services/sqlstore/sqlbuilder.go index 9a0dd0d3989..6274458818e 100644 --- a/pkg/services/sqlstore/sqlbuilder.go +++ b/pkg/services/sqlstore/sqlbuilder.go @@ -12,7 +12,7 @@ type SqlBuilder struct { params []interface{} } -func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, minPermission m.PermissionType) { +func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permission m.PermissionType) { if user.OrgRole == m.ROLE_ADMIN { return @@ -40,6 +40,6 @@ func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, minPe ) )`) - sb.params = append(sb.params, user.OrgId, minPermission, user.UserId, user.UserId) + sb.params = append(sb.params, user.OrgId, permission, user.UserId, user.UserId) sb.params = append(sb.params, okRoles...) } diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts index 56284a877c5..0e5c22c4db2 100644 --- a/public/app/features/dashboard/folder_picker/folder_picker.ts +++ b/public/app/features/dashboard/folder_picker/folder_picker.ts @@ -30,7 +30,13 @@ export class FolderPickerCtrl { } getOptions(query) { - return this.backendSrv.get('api/dashboards/folders', { query: query }).then(result => { + const params = { + query: query, + type: 'dash-folder', + permission: 'Edit', + }; + + return this.backendSrv.get('api/search', params).then(result => { if ( query === '' || query.toLowerCase() === 'g' || From 4d5a24a6c4a9604174f4b0f2a2c948ae581b88c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 9 Feb 2018 17:24:34 +0100 Subject: [PATCH 03/73] permissions: might have a solution for search --- pkg/services/sqlstore/sqlbuilder.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pkg/services/sqlstore/sqlbuilder.go b/pkg/services/sqlstore/sqlbuilder.go index 6274458818e..6f3b32be5e7 100644 --- a/pkg/services/sqlstore/sqlbuilder.go +++ b/pkg/services/sqlstore/sqlbuilder.go @@ -24,6 +24,26 @@ func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permi okRoles = append(okRoles, m.ROLE_VIEWER) } + // SELECT dash.id, dash.title, dash.folder_id + // FROM dashboard AS dash + // LEFT JOIN dashboard folder on folder.id = dash.folder_id + // LEFT JOIN dashboard_acl AS da ON + // da.dashboard_id = dash.id OR + // da.dashboard_id = dash.folder_id OR + // ( + // -- include default permissions --> + // da.org_id = -1 AND (folder.has_acl = 0 OR (dash.has_acl = 0 AND dash.folder_id = 0)) + // ) + // LEFT JOIN team_member as ugm on ugm.team_id = da.team_id + // WHERE + // dash.org_id = 5 AND + // ( + // da.user_id = 8 or + // ugm.user_id = 8 or + // da.role in ('Viewer', 'Editor') + // ) AND + // da.permission > 1 + // sb.sql.WriteString(` AND ( dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR From acd085605c86bd09101b20db97c87b05403f047a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Sat, 10 Feb 2018 16:21:36 +0100 Subject: [PATCH 04/73] docs: spelling. --- docs/sources/features/datasources/cloudwatch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index 648957ed96e..e955dbb9569 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -13,7 +13,7 @@ weight = 10 # Using AWS CloudWatch in Grafana -Grafana ships with built in support for CloudWatch. You just have to add it as a data source and you will be ready to build dashboards for you CloudWatch metrics. +Grafana ships with built in support for CloudWatch. You just have to add it as a data source and you will be ready to build dashboards for your CloudWatch metrics. ## Adding the data source to Grafana From 5fced6e92b6128bd24ab2dd986c870e18afa3c38 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 12 Feb 2018 16:10:23 +0100 Subject: [PATCH 05/73] added buttons and text to empty dashboard list --- .../components/manage_dashboards/manage_dashboards.html | 8 +++++++- .../components/manage_dashboards/manage_dashboards.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index effe9f66930..2df60fb9a6c 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -1,5 +1,5 @@
-
+
+
+ + This org has no dashboards or folders. + +
+
+ + This org has no dashboards or folders. + +
-
- - This org has no dashboards or folders. - -
+
+
An email with a reset link as been sent to the email address.
You should receive it shortly.
@@ -27,5 +27,23 @@
+
From 42af87b7c9c0b25a3f59cbcd6718fc6d5bade75a Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 12 Feb 2018 17:10:27 +0100 Subject: [PATCH 08/73] fixed bg gradient, fixes #10869 (#10875) --- public/sass/_variables.light.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index e3f442b8908..eb598f27d4c 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -72,7 +72,7 @@ $textShadow: none; // gradients $brand-gradient: linear-gradient(to right, rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); -$page-gradient: linear-gradient(-60deg, transparent 70%, $gray-7 98%); +$page-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%); // Links // ------------------------- From aaf4e760c652d3b15681083cd6b0253e6346ef3a Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 12 Feb 2018 17:11:41 +0100 Subject: [PATCH 09/73] new dashboard is now hidden from viewer, fixes #10815 (#10854) * new dashboard is now hidden from viewer, fixes #10815 * changed orgRole to isEditor * removed unused import * added contextSrv mock to search.jest --- public/app/core/components/search/search.html | 2 +- public/app/core/components/search/search.ts | 3 +++ public/app/core/specs/search.jest.ts | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index e0106740d3a..acaf0730a6b 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -43,7 +43,7 @@ -
+
New dashboard diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 04b77e7b7fe..25e05c2139d 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; import coreModule from '../../core_module'; import { SearchSrv } from 'app/core/services/search_srv'; +import { contextSrv } from 'app/core/services/context_srv'; import appEvents from 'app/core/app_events'; export class SearchCtrl { @@ -15,6 +16,7 @@ export class SearchCtrl { ignoreClose: any; isLoading: boolean; initialFolderFilterTitle: string; + isEditor: string; /** @ngInject */ constructor($scope, private $location, private $timeout, private searchSrv: SearchSrv) { @@ -24,6 +26,7 @@ export class SearchCtrl { this.initialFolderFilterTitle = 'All'; this.getTags = this.getTags.bind(this); this.onTagSelect = this.onTagSelect.bind(this); + this.isEditor = contextSrv.isEditor; } closeSearch() { diff --git a/public/app/core/specs/search.jest.ts b/public/app/core/specs/search.jest.ts index 2c1172bcf55..8aea35af213 100644 --- a/public/app/core/specs/search.jest.ts +++ b/public/app/core/specs/search.jest.ts @@ -1,6 +1,12 @@ import { SearchCtrl } from '../components/search/search'; import { SearchSrv } from '../services/search_srv'; +jest.mock('app/core/services/context_srv', () => ({ + contextSrv: { + user: { orgId: 1 }, + }, +})); + describe('SearchCtrl', () => { const searchSrvStub = { search: (options: any) => {}, From e57c8d0357ed4b00f8ec593131303c07956720c6 Mon Sep 17 00:00:00 2001 From: Scott Brenner Date: Mon, 12 Feb 2018 13:59:19 -0800 Subject: [PATCH 10/73] Minor typo fix Minor typo fix --- conf/provisioning/datasources/sample.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/provisioning/datasources/sample.yaml b/conf/provisioning/datasources/sample.yaml index 1bb9cb53b45..c1d3c2e1fb7 100644 --- a/conf/provisioning/datasources/sample.yaml +++ b/conf/provisioning/datasources/sample.yaml @@ -4,7 +4,7 @@ # org_id: 1 # # list of datasources to insert/update depending -# # whats available in the datbase +# # on what's available in the datbase #datasources: # # name of the datasource. Required # - name: Graphite From ba0285a3ecea090445605cd9bc6cd97ec5826c0b Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 13 Feb 2018 15:47:02 +0100 Subject: [PATCH 11/73] provisioning: Warns the user when uid or title is re-used. (#10892) * provisioning: Warns the user when uid or title is re-used. Closes #10880 --- docs/sources/administration/provisioning.md | 9 ++- .../provisioning/dashboards/file_reader.go | 72 ++++++++++++++++--- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index c3595969281..b70895cd7bc 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -155,7 +155,7 @@ Since not all datasources have the same configuration settings we only have the #### Secure Json data -{"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"} +`{"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"}` Secure json data is a map of settings that will be encrypted with [secret key](/installation/configuration/#secret-key) from the Grafana config. The purpose of this is only to hide content from the users of the application. This should be used for storing TLS Cert and password that Grafana will append to the request on the server side. All of these settings are optional. @@ -169,7 +169,7 @@ Secure json data is a map of settings that will be encrypted with [secret key](/ ### Dashboards -It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into Grafana. Currently we only support reading dashboards from file but we will add more providers in the future. +It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into Grafana from the local filesystem. The dashboard provider config file looks somewhat like this: @@ -183,3 +183,8 @@ The dashboard provider config file looks somewhat like this: ``` When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated. + +> **Note.** Provisioning allows you to overwrite existing dashboards +> which leads to problems if you re-use settings that are supposed to be unique. +> Be careful not to re-use the same `title` multiple times within a folder +> or `uid` within the same installation as this will cause weird behaviours. diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index c67f355a36e..c909878999e 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -124,37 +124,48 @@ func (fr *fileReader) startWalkingDisk() error { } } + sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name) + // save dashboards based on json files for path, fileInfo := range filesFoundOnDisk { - err = fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs) + provisioningMetadata, err := fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs) + sanityChecker.track(provisioningMetadata) if err != nil { fr.log.Error("failed to save dashboard", "error", err) } } + sanityChecker.logWarnings(fr.log) return nil } -func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) error { +func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) (provisioningMetadata, error) { + provisioningMetadata := provisioningMetadata{} resolvedFileInfo, err := resolveSymlink(fileInfo, path) if err != nil { - return err + return provisioningMetadata, err } provisionedData, alreadyProvisioned := provisionedDashboardRefs[path] - if alreadyProvisioned && provisionedData.Updated.Unix() == resolvedFileInfo.ModTime().Unix() { - return nil // dashboard is already in sync with the database - } + upToDate := alreadyProvisioned && provisionedData.Updated.Unix() == resolvedFileInfo.ModTime().Unix() dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId) if err != nil { fr.log.Error("failed to load dashboard from ", "file", path, "error", err) - return nil + return provisioningMetadata, nil + } + + // keeps track of what uid's and title's we have already provisioned + provisioningMetadata.uid = dash.Dashboard.Uid + provisioningMetadata.title = dash.Dashboard.Title + + if upToDate { + return provisioningMetadata, nil } if dash.Dashboard.Id != 0 { fr.log.Error("provisioned dashboard json files cannot contain id") - return nil + return provisioningMetadata, nil } if alreadyProvisioned { @@ -164,7 +175,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil fr.log.Debug("saving new dashboard", "file", path) dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime()} _, err = fr.dashboardRepo.SaveProvisionedDashboard(dash, dp) - return err + return provisioningMetadata, err } func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) { @@ -280,3 +291,46 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, return dash, nil } + +type provisioningMetadata struct { + uid string + title string +} + +func newProvisioningSanityChecker(provisioningProvider string) provisioningSanityChecker { + return provisioningSanityChecker{ + provisioningProvider: provisioningProvider, + uidUsage: map[string]uint8{}, + titleUsage: map[string]uint8{}} +} + +type provisioningSanityChecker struct { + provisioningProvider string + uidUsage map[string]uint8 + titleUsage map[string]uint8 +} + +func (checker provisioningSanityChecker) track(pm provisioningMetadata) { + if len(pm.uid) > 0 { + checker.uidUsage[pm.uid] += 1 + } + if len(pm.title) > 0 { + checker.titleUsage[pm.title] += 1 + } + +} + +func (checker provisioningSanityChecker) logWarnings(log log.Logger) { + for uid, times := range checker.uidUsage { + if times > 1 { + log.Error("the same 'uid' is used more than once", "uid", uid, "provider", checker.provisioningProvider) + } + } + + for title, times := range checker.titleUsage { + if times > 1 { + log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider) + } + } + +} From e3c3f3ce4ca2f02424c393c632686b6c49188032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 13 Feb 2018 16:49:00 +0100 Subject: [PATCH 12/73] fix: sql search permissions filter fix --- .../sqlstore/dashboard_folder_test.go | 38 +++++++++------ pkg/services/sqlstore/dashboard_test.go | 11 +++-- pkg/services/sqlstore/search_builder.go | 5 +- pkg/services/sqlstore/sqlbuilder.go | 46 ++++++++----------- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index bd09e7490cb..b32a4dfed1d 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -26,7 +26,11 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and no acls are set", func() { Convey("should return all dashboards", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, + OrgId: 1, + DashboardIds: []int64{folder.Id, dashInRoot.Id}, + } err := SearchDashboards(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -40,7 +44,10 @@ func TestDashboardFolderDataAccess(t *testing.T) { updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) Convey("should not return folder", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, + OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}, + } err := SearchDashboards(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) @@ -51,7 +58,11 @@ func TestDashboardFolderDataAccess(t *testing.T) { updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT) Convey("should be able to access folder", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, + OrgId: 1, + DashboardIds: []int64{folder.Id, dashInRoot.Id}, + } err := SearchDashboards(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -87,7 +98,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT) Convey("should not return folder or child", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} err := SearchDashboards(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) @@ -98,7 +109,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT) Convey("should be able to search for child dashboard but not folder", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} err := SearchDashboards(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -141,7 +152,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and one folder is expanded, the other collapsed", func() { Convey("should return dashboards in root and expanded folder", func() { - query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1} + query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1} err := SearchDashboards(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 4) @@ -162,7 +173,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("should not return folder with acl or its children", func() { query := &search.FindPersistedDashboardsQuery{ - SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, } @@ -172,14 +183,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) }) }) - Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() { + movedDash := moveDashboard(1, childDash1.Data, folder2.Id) So(movedDash.HasAcl, ShouldBeFalse) Convey("should return folder without acl and its children", func() { query := &search.FindPersistedDashboardsQuery{ - SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, } @@ -200,16 +211,17 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("should return folder without acl but not the dashboard with acl", func() { query := &search.FindPersistedDashboardsQuery{ - SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, } err := SearchDashboards(query) So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 3) + So(len(query.Result), ShouldEqual, 4) So(query.Result[0].Id, ShouldEqual, folder2.Id) - So(query.Result[1].Id, ShouldEqual, childDash2.Id) - So(query.Result[2].Id, ShouldEqual, dashInRoot.Id) + So(query.Result[1].Id, ShouldEqual, childDash1.Id) + So(query.Result[2].Id, ShouldEqual, childDash2.Id) + So(query.Result[3].Id, ShouldEqual, dashInRoot.Id) }) }) }) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index bd769d307eb..de7cdf19927 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -512,7 +512,7 @@ func TestDashboardDataAccess(t *testing.T) { query := search.FindPersistedDashboardsQuery{ Title: "1 test dash folder", OrgId: 1, - SignedInUser: &m.SignedInUser{OrgId: 1}, + SignedInUser: &m.SignedInUser{OrgId: 1, OrgRole: m.ROLE_EDITOR}, } err := SearchDashboards(&query) @@ -529,7 +529,7 @@ func TestDashboardDataAccess(t *testing.T) { query := search.FindPersistedDashboardsQuery{ OrgId: 1, FolderIds: []int64{savedFolder.Id}, - SignedInUser: &m.SignedInUser{OrgId: 1}, + SignedInUser: &m.SignedInUser{OrgId: 1, OrgRole: m.ROLE_EDITOR}, } err := SearchDashboards(&query) @@ -549,7 +549,7 @@ func TestDashboardDataAccess(t *testing.T) { Convey("should be able to find two dashboards by id", func() { query := search.FindPersistedDashboardsQuery{ DashboardIds: []int64{2, 3}, - SignedInUser: &m.SignedInUser{OrgId: 1}, + SignedInUser: &m.SignedInUser{OrgId: 1, OrgRole: m.ROLE_EDITOR}, } err := SearchDashboards(&query) @@ -578,7 +578,10 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to search for starred dashboards", func() { - query := search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: 10, OrgId: 1}, IsStarred: true} + query := search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: 10, OrgId: 1, OrgRole: m.ROLE_EDITOR}, + IsStarred: true, + } err := SearchDashboards(&query) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index 0db0fca53b0..ddfbfbfc551 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -153,10 +153,7 @@ func (sb *SearchBuilder) buildMainQuery() { sb.sql.WriteString(` WHERE `) sb.buildSearchWhereClause() - sb.sql.WriteString(` - LIMIT ?) as ids - INNER JOIN dashboard on ids.id = dashboard.id - `) + sb.sql.WriteString(` LIMIT ?) as ids INNER JOIN dashboard on ids.id = dashboard.id `) sb.params = append(sb.params, sb.limit) } diff --git a/pkg/services/sqlstore/sqlbuilder.go b/pkg/services/sqlstore/sqlbuilder.go index 6f3b32be5e7..b38bd693e66 100644 --- a/pkg/services/sqlstore/sqlbuilder.go +++ b/pkg/services/sqlstore/sqlbuilder.go @@ -24,39 +24,33 @@ func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permi okRoles = append(okRoles, m.ROLE_VIEWER) } - // SELECT dash.id, dash.title, dash.folder_id - // FROM dashboard AS dash - // LEFT JOIN dashboard folder on folder.id = dash.folder_id - // LEFT JOIN dashboard_acl AS da ON - // da.dashboard_id = dash.id OR - // da.dashboard_id = dash.folder_id OR - // ( - // -- include default permissions --> - // da.org_id = -1 AND (folder.has_acl = 0 OR (dash.has_acl = 0 AND dash.folder_id = 0)) - // ) - // LEFT JOIN team_member as ugm on ugm.team_id = da.team_id - // WHERE - // dash.org_id = 5 AND - // ( - // da.user_id = 8 or - // ugm.user_id = 8 or - // da.role in ('Viewer', 'Editor') - // ) AND - // da.permission > 1 - // + falseStr := dialect.BooleanStr(false) + sb.sql.WriteString(` AND ( - dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR - dashboard.id in ( + dashboard.id IN ( SELECT distinct d.id AS DashboardId FROM dashboard AS d - LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id - LEFT JOIN team_member as ugm on ugm.team_id = da.team_id + LEFT JOIN dashboard folder on folder.id = d.folder_id + LEFT JOIN dashboard_acl AS da ON + da.dashboard_id = d.id OR + da.dashboard_id = d.folder_id OR + ( + -- include default permissions --> + da.org_id = -1 AND ( + (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + ) + ) + LEFT JOIN team_member as ugm on ugm.team_id = da.team_id WHERE - d.has_acl = ` + dialect.BooleanStr(true) + ` AND d.org_id = ? AND da.permission >= ? AND - (da.user_id = ? or ugm.user_id = ? or da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)) + ( + da.user_id = ? OR + ugm.user_id = ? OR + da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `) + ) ) )`) From 162439a8d63756696071349a838feca048731bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 13 Feb 2018 17:03:20 +0100 Subject: [PATCH 13/73] fix: removed logging --- pkg/services/sqlstore/dashboard.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 05cce3f001f..a681b7273d3 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -1,7 +1,6 @@ package sqlstore import ( - "fmt" "strings" "time" @@ -312,8 +311,6 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear var res []DashboardSearchProjection sql, params := sb.ToSql() - fmt.Printf("%s, %v", sql, params) - sqlog.Info("sql", "sql", sql, "params", params) err := x.Sql(sql, params...).Find(&res) if err != nil { return nil, err From e9380bbdffdec47ebf921b8b7e82ddb355be29a8 Mon Sep 17 00:00:00 2001 From: Jan Fajerski Date: Tue, 13 Feb 2018 17:20:51 +0100 Subject: [PATCH 14/73] sass/base: import from current dir in _fonts.scss (#10894) The import statement relative to base seems to confuse sass if there is a colon (:) in the path name. Fixes: #10866 Signed-off-by: Jan Fajerski --- public/sass/base/_fonts.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/sass/base/_fonts.scss b/public/sass/base/_fonts.scss index 4e680872b5a..aab86329612 100644 --- a/public/sass/base/_fonts.scss +++ b/public/sass/base/_fonts.scss @@ -1,5 +1,5 @@ -@import "base/font_awesome"; -@import "base/grafana_icons"; +@import "font_awesome"; +@import "grafana_icons"; /* cyrillic-ext */ @font-face { From 6a85369c50513c5afc5a80210ff5c7599702abfb Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 14 Feb 2018 01:22:55 +0900 Subject: [PATCH 15/73] add 13-24 for min width (#10891) --- public/app/partials/panelgeneral.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/partials/panelgeneral.html b/public/app/partials/panelgeneral.html index 2054aea1e7f..eb17d743d33 100644 --- a/public/app/partials/panelgeneral.html +++ b/public/app/partials/panelgeneral.html @@ -20,7 +20,7 @@
Min width -
From 3d91a1cbdd0553cdfe31af3ff276ce3b9f7e797a Mon Sep 17 00:00:00 2001 From: stukselbax Date: Wed, 14 Feb 2018 10:53:14 +0300 Subject: [PATCH 16/73] Duplicate typo fixed --- docs/sources/http_api/dashboard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 0538754bd96..6ddb2360e03 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -83,7 +83,7 @@ Content-Length: 97 } ``` -In in case of title already exists the `status` property will be `name-exists`. +In case of title already exists the `status` property will be `name-exists`. ## Get dashboard From b0fae0129c616c71440e6fd8c1f21289df236be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Feb 2018 09:37:39 +0100 Subject: [PATCH 17/73] ux: refactoring #10884 --- .../manage_dashboards/manage_dashboards.html | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 7e1c5fceecf..2dfb9c96d1b 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -1,5 +1,5 @@
-
+
+
+ + No dashboards found. + +
+
-
- - This org has no dashboards or folders. - -
- -
{ + it('will clear state', () => { + return setup.clearState(); + }); +}); diff --git a/tests/api/client.ts b/tests/api/client.ts new file mode 100644 index 00000000000..2e2f9ed67fd --- /dev/null +++ b/tests/api/client.ts @@ -0,0 +1,30 @@ +const axios = require('axios'); + +export function getClient(options) { + return axios.create({ + baseURL: 'http://localhost:3000', + timeout: 1000, + auth: { + username: options.username, + password: options.password, + }, + }); +} + +export function getAdminClient() { + return getClient({ + username: 'admin', + password: 'admin', + }); +} + +let client = getAdminClient(); + +client.callAs = function(user) { + return getClient({ + username: user.login, + password: 'password', + }); +}; + +export default client; diff --git a/tests/api/dashboard.test.ts b/tests/api/dashboard.test.ts new file mode 100644 index 00000000000..55beaffecb0 --- /dev/null +++ b/tests/api/dashboard.test.ts @@ -0,0 +1,45 @@ +import client from './client'; +import * as setup from './setup'; + +describe('/api/dashboards', () => { + let state: any = {}; + + beforeAll(async () => { + state = await setup.ensureState({ + orgName: 'api-test-org', + users: [ + { user: setup.admin, role: 'Admin' }, + { user: setup.editor, role: 'Editor' }, + { user: setup.viewer, role: 'Viewer' }, + ], + admin: setup.admin, + dashboards: [ + { + title: 'aaa', + uid: 'aaa', + }, + { + title: 'bbb', + uid: 'bbb', + }, + ], + }); + }); + + describe('With admin user', () => { + it('can delete dashboard', async () => { + let rsp = await client.callAs(setup.admin).delete(`/api/dashboards/uid/aaa`); + expect(rsp.data.title).toBe('aaa'); + }); + }); + + describe('With viewer user', () => { + it('Cannot delete dashboard', async () => { + let rsp = await setup.expectError(() => { + return client.callAs(setup.viewer).delete(`/api/dashboards/uid/bbb`); + }); + + expect(rsp.response.status).toBe(403); + }); + }); +}); diff --git a/tests/api/jest.js b/tests/api/jest.js new file mode 100644 index 00000000000..b32573115b7 --- /dev/null +++ b/tests/api/jest.js @@ -0,0 +1,19 @@ +module.exports = { + verbose: true, + "globals": { + "ts-jest": { + "tsConfigFile": "tsconfig.json" + } + }, + "transform": { + "^.+\\.tsx?$": "/../../node_modules/ts-jest/preprocessor.js" + }, + "moduleDirectories": ["node_modules"], + "testRegex": "(\\.|/)(test)\\.ts$", + "testEnvironment": "node", + "moduleFileExtensions": [ + "ts", + "js", + "json" + ], +}; diff --git a/tests/api/search.test.ts b/tests/api/search.test.ts new file mode 100644 index 00000000000..91d1ebf0d35 --- /dev/null +++ b/tests/api/search.test.ts @@ -0,0 +1,27 @@ +import client from './client'; +import * as setup from './setup'; + +describe('GET /api/search', () => { + const state = {}; + + beforeAll(async () => { + state = await setup.ensureState({ + orgName: 'api-test-org', + users: [{ user: setup.admin, role: 'Admin' }], + admin: setup.admin, + dashboards: [ + { + title: 'Dashboard in root no permissions', + uid: 'AAA', + }, + ], + }); + }); + + describe('With admin user', () => { + it('should return all dashboards', async () => { + let rsp = await client.callAs(state.admin).get('/api/search'); + expect(rsp.data).toHaveLength(1); + }); + }); +}); diff --git a/tests/api/setup.ts b/tests/api/setup.ts new file mode 100644 index 00000000000..0566729999c --- /dev/null +++ b/tests/api/setup.ts @@ -0,0 +1,107 @@ +import client from './client'; +import _ from 'lodash;'; + +export const editor = { + email: 'api-test-editor@grafana.com', + login: 'api-test-editor', + password: 'password', + name: 'Api Test Editor', +}; + +export const admin = { + email: 'api-test-admin@grafana.com', + login: 'api-test-admin', + password: 'password', + name: 'Api Test Super', +}; + +export const viewer = { + email: 'api-test-viewer@grafana.com', + login: 'api-test-viewer', + password: 'password', + name: 'Api Test Viewer', +}; + +export async function expectError(callback) { + try { + let rsp = await callback(); + return rsp; + } catch (err) { + return err; + } + + return rsp; +} + +// deletes org if it's already there +export async function getOrg(orgName) { + try { + const rsp = await client.get(`/api/orgs/name/${orgName}`); + await client.delete(`/api/orgs/${rsp.data.id}`); + } catch {} + + const rsp = await client.post(`/api/orgs`, { name: orgName }); + return { name: orgName, id: rsp.data.orgId }; +} + +export async function getUser(user) { + const search = await client.get('/api/users/search', { + params: { query: user.login }, + }); + + if (search.data.totalCount === 1) { + user.id = search.data.users[0].id; + return user; + } + + const rsp = await client.post('/api/admin/users', user); + user.id = rsp.data.id; + + return user; +} + +export async function addUserToOrg(org, user, role) { + const rsp = await client.post(`/api/orgs/${org.id}/users`, { + loginOrEmail: user.login, + role: role, + }); + + return rsp.data; +} + +export async function clearState() { + const admin = await getUser(adminUser); + const rsp = await client.delete(`/api/admin/users/${admin.id}`); + return rsp.data; +} + +export async function setUsingOrg(user, org) { + await client.callAs(user).post(`/api/user/using/${org.id}`); +} + +export async function createDashboard(user, dashboard) { + const rsp = await client.callAs(user).post(`/api/dashboards/db`, { + dashboard: dashboard, + overwrite: true, + }); + dashboard.id = rsp.data.id; + dashboard.url = rsp.data.url; + + return dashboard; +} + +export async function ensureState(state) { + const org = await getOrg(state.orgName); + + for (let orgUser of state.users) { + const user = await getUser(orgUser.user); + await addUserToOrg(org, user, orgUser.role); + await setUsingOrg(user, org); + } + + for (let dashboard of state.dashboards) { + await createDashboard(state.admin, dashboard); + } + + return state; +} diff --git a/tests/api/tsconfig.json b/tests/api/tsconfig.json new file mode 100644 index 00000000000..3dd8c94d7d0 --- /dev/null +++ b/tests/api/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "target": "es6", + "lib": ["es6"], + "module": "commonjs", + "declaration": false, + "allowSyntheticDefaultImports": true, + "inlineSourceMap": false, + "sourceMap": true, + "noEmitOnError": false, + "emitDecoratorMetadata": false, + "experimentalDecorators": true, + "noImplicitReturns": true, + "noImplicitThis": false, + "noImplicitUseStrict":false, + "noImplicitAny": false, + "noUnusedLocals": true + }, + "include": [ + "*.ts", + "**/*.ts" + ] +} diff --git a/tests/api/user.test.ts b/tests/api/user.test.ts new file mode 100644 index 00000000000..ef1c927c69e --- /dev/null +++ b/tests/api/user.test.ts @@ -0,0 +1,22 @@ +import client from './client'; +import * as setup from './setup'; + +describe('GET /api/user', () => { + it('should return current authed user', async () => { + let rsp = await client.get('/api/user'); + expect(rsp.data.login).toBe('admin'); + }); +}); + +describe('PUT /api/user', () => { + it('should update current authed user', async () => { + const user = await setup.getUser(setup.editor); + user.name = 'Updated via test'; + + const rsp = await client.callAs(user).put('/api/user', user); + expect(rsp.data.message).toBe('User updated'); + + const updated = await client.callAs(user).get('/api/user'); + expect(updated.data.name).toBe('Updated via test'); + }); +}); diff --git a/yarn.lock b/yarn.lock index e95968736ba..83a906c919d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -224,6 +224,14 @@ version "16.0.25" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.25.tgz#bf696b83fe480c5e0eff4335ee39ebc95884a1ed" +"@types/strip-bom@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" + +"@types/strip-json-comments@0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" + JSONStream@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -699,6 +707,13 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +axios@^0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d" + dependencies: + follow-redirects "^1.2.5" + is-buffer "^1.1.5" + babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -1626,6 +1641,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +chalk@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" + dependencies: + ansi-styles "^3.2.0" + escape-string-regexp "^1.0.5" + supports-color "^5.2.0" + chalk@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" @@ -2776,7 +2799,7 @@ diff@^2.0.2: version "2.2.3" resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" -diff@^3.2.0: +diff@^3.1.0, diff@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" @@ -3748,6 +3771,12 @@ flush-write-stream@^1.0.0: inherits "^2.0.1" readable-stream "^2.0.4" +follow-redirects@^1.2.5: + version "1.4.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" + dependencies: + debug "^3.1.0" + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -4385,6 +4414,10 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -4513,6 +4546,12 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" +homedir-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + dependencies: + parse-passwd "^1.0.0" + hooker@^0.2.3, hooker@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" @@ -6211,6 +6250,10 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" +make-error@^1.1.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.3.tgz#a97ae14ffd98b05f543e83ddc395e1b2b6e4cc6a" + make-fetch-happen@^2.4.13, make-fetch-happen@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38" @@ -7371,6 +7414,10 @@ parse-json@^3.0.0: dependencies: error-ex "^1.3.1" +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + parse5@^3.0.1, parse5@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" @@ -9601,7 +9648,7 @@ strip-json-comments@1.0.x, strip-json-comments@~1.0.1, strip-json-comments@~1.0. version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" -strip-json-comments@~2.0.1: +strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -9633,6 +9680,12 @@ supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: dependencies: has-flag "^2.0.0" +supports-color@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" + dependencies: + has-flag "^3.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -9941,6 +9994,30 @@ ts-loader@^3.2.0: loader-utils "^1.0.2" semver "^5.0.1" +ts-node@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-4.1.0.tgz#36d9529c7b90bb993306c408cd07f7743de20712" + dependencies: + arrify "^1.0.0" + chalk "^2.3.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.0" + tsconfig "^7.0.0" + v8flags "^3.0.0" + yn "^2.0.0" + +tsconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" + dependencies: + "@types/strip-bom" "^3.0.0" + "@types/strip-json-comments" "0.0.30" + strip-bom "^3.0.0" + strip-json-comments "^2.0.0" + tslib@^1.7.1: version "1.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" @@ -10302,6 +10379,12 @@ uuid@^3.0.0, uuid@^3.1.0, uuid@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +v8flags@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b" + dependencies: + homedir-polyfill "^1.0.1" + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" @@ -10766,6 +10849,10 @@ yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" + zip-stream@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04" From 4a35244cb9d1257bd639a16ee7f1f9b2b3dcfcc5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 12 Feb 2018 15:17:32 +0100 Subject: [PATCH 19/73] provisioning: support camcelCase provisioning files --- conf/provisioning/datasources/sample.yaml | 25 +-- docs/sources/administration/provisioning.md | 27 +-- .../provisioning/datasources/config_reader.go | 105 ++++++++++++ ...asources_test.go => config_reader_test.go} | 80 ++++++--- .../provisioning/datasources/datasources.go | 66 -------- .../all-properties/all-properties.yaml | 23 ++- .../test-configs/all-properties/second.yaml | 2 +- .../test-configs/version-0/version-0.yaml | 28 ++++ .../provisioning/datasources/types.go | 158 +++++++++++++++++- 9 files changed, 384 insertions(+), 130 deletions(-) create mode 100644 pkg/services/provisioning/datasources/config_reader.go rename pkg/services/provisioning/datasources/{datasources_test.go => config_reader_test.go} (71%) create mode 100644 pkg/services/provisioning/datasources/test-configs/version-0/version-0.yaml diff --git a/conf/provisioning/datasources/sample.yaml b/conf/provisioning/datasources/sample.yaml index c1d3c2e1fb7..cffeb3e0d2d 100644 --- a/conf/provisioning/datasources/sample.yaml +++ b/conf/provisioning/datasources/sample.yaml @@ -1,7 +1,10 @@ +# # config file version +# apiVersion: 1 + # # list of datasources that should be deleted from the database -#delete_datasources: +#deleteDatasources: # - name: Graphite -# org_id: 1 +# orgId: 1 # # list of datasources to insert/update depending # # on what's available in the datbase @@ -12,8 +15,8 @@ # type: graphite # # access mode. direct or proxy. Required # access: proxy -# # org id. will default to org_id 1 if not specified -# org_id: 1 +# # org id. will default to orgId 1 if not specified +# orgId: 1 # # url # url: http://localhost:8080 # # database password, if used @@ -23,22 +26,22 @@ # # database name, if used # database: # # enable/disable basic auth -# basic_auth: +# basicAuth: # # basic auth username -# basic_auth_user: +# basicAuthUser: # # basic auth password -# basic_auth_password: +# basicAuthPassword: # # enable/disable with credentials headers -# with_credentials: +# withCredentials: # # mark as default datasource. Max one per org -# is_default: +# isDefault: # # fields that will be converted to json and stored in json_data -# json_data: +# jsonData: # graphiteVersion: "1.1" # tlsAuth: true # tlsAuthWithCACert: true # # json object of data that will be encrypted. -# secure_json_data: +# secureJsonData: # tlsCACert: "..." # tlsClientCert: "..." # tlsClientKey: "..." diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index b70895cd7bc..e783798b97d 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -81,13 +81,16 @@ If you are running multiple instances of Grafana you might run into problems if ### Example datasource config file ```yaml +# config file version +apiVersion: 1 + # list of datasources that should be deleted from the database -delete_datasources: +deleteDatasources: - name: Graphite - org_id: 1 + orgId: 1 # list of datasources to insert/update depending -# whats available in the datbase +# whats available in the database datasources: # name of the datasource. Required - name: Graphite @@ -95,8 +98,8 @@ datasources: type: graphite # access mode. direct or proxy. Required access: proxy - # org id. will default to org_id 1 if not specified - org_id: 1 + # org id. will default to orgId 1 if not specified + orgId: 1 # url url: http://localhost:8080 # database password, if used @@ -106,22 +109,22 @@ datasources: # database name, if used database: # enable/disable basic auth - basic_auth: + basicAuth: # basic auth username - basic_auth_user: + basicAuthUser: # basic auth password - basic_auth_password: + basicAuthPassword: # enable/disable with credentials headers - with_credentials: + withCredentials: # mark as default datasource. Max one per org - is_default: + isDefault: # fields that will be converted to json and stored in json_data - json_data: + jsonData: graphiteVersion: "1.1" tlsAuth: true tlsAuthWithCACert: true # json object of data that will be encrypted. - secure_json_data: + secureJsonData: tlsCACert: "..." tlsClientCert: "..." tlsClientKey: "..." diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go new file mode 100644 index 00000000000..434b487bd2d --- /dev/null +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -0,0 +1,105 @@ +package datasources + +import ( + "io/ioutil" + "path/filepath" + "strings" + + "github.com/grafana/grafana/pkg/log" + "gopkg.in/yaml.v2" +) + +type configReader struct { + log log.Logger +} + +func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) { + var datasources []*DatasourcesAsConfig + + files, err := ioutil.ReadDir(path) + if err != nil { + cr.log.Error("cant read datasource provisioning files from directory", "path", path) + return datasources, nil + } + + for _, file := range files { + if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") { + filename, _ := filepath.Abs(filepath.Join(path, file.Name())) + yamlFile, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + datasource, err := parseDatasourceConfig(yamlFile) + if err != nil { + return nil, err + } + + if datasource != nil { + datasources = append(datasources, datasource) + } + } + } + + err = validateDefaultUniqueness(datasources) + if err != nil { + return nil, err + } + + return datasources, nil +} + +func parseDatasourceConfig(yamlFile []byte) (*DatasourcesAsConfig, error) { + var apiVersion *ConfigVersion + err := yaml.Unmarshal(yamlFile, &apiVersion) + if err != nil { + return nil, err + } + + if apiVersion.ApiVersion > 0 { + var v1 *DatasourcesAsConfigV1 + err = yaml.Unmarshal(yamlFile, &v1) + if err != nil { + return nil, err + } + return v1.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil + + } + + var v0 *DatasourcesAsConfigV0 + err = yaml.Unmarshal(yamlFile, &v0) + if err != nil { + return nil, err + } + return v0.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil +} + +func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error { + defaultCount := 0 + for i := range datasources { + if datasources[i].Datasources == nil { + continue + } + + for _, ds := range datasources[i].Datasources { + if ds.OrgId == 0 { + ds.OrgId = 1 + } + + if ds.IsDefault { + defaultCount++ + if defaultCount > 1 { + return ErrInvalidConfigToManyDefault + } + } + } + + for _, ds := range datasources[i].DeleteDatasources { + if ds.OrgId == 0 { + ds.OrgId = 1 + } + } + } + + return nil +} diff --git a/pkg/services/provisioning/datasources/datasources_test.go b/pkg/services/provisioning/datasources/config_reader_test.go similarity index 71% rename from pkg/services/provisioning/datasources/datasources_test.go rename to pkg/services/provisioning/datasources/config_reader_test.go index 00dc59f6617..9a0419232ac 100644 --- a/pkg/services/provisioning/datasources/datasources_test.go +++ b/pkg/services/provisioning/datasources/config_reader_test.go @@ -17,6 +17,7 @@ var ( twoDatasourcesConfigPurgeOthers string = "./test-configs/insert-two-delete-two" doubleDatasourcesConfig string = "./test-configs/double-default" allProperties string = "./test-configs/all-properties" + versionZero string = "./test-configs/version-0" brokenYaml string = "./test-configs/broken-yaml" fakeRepo *fakeRepository @@ -130,7 +131,7 @@ func TestDatasourceAsConfig(t *testing.T) { So(len(cfg), ShouldEqual, 0) }) - Convey("can read all properties", func() { + Convey("can read all properties from version 1", func() { cfgProvifer := &configReader{log: log.New("test logger")} cfg, err := cfgProvifer.readConfig(allProperties) if err != nil { @@ -140,38 +141,65 @@ func TestDatasourceAsConfig(t *testing.T) { So(len(cfg), ShouldEqual, 2) dsCfg := cfg[0] - ds := dsCfg.Datasources[0] - So(ds.Name, ShouldEqual, "name") - So(ds.Type, ShouldEqual, "type") - So(ds.Access, ShouldEqual, models.DS_ACCESS_PROXY) - So(ds.OrgId, ShouldEqual, 2) - So(ds.Url, ShouldEqual, "url") - So(ds.User, ShouldEqual, "user") - So(ds.Password, ShouldEqual, "password") - So(ds.Database, ShouldEqual, "database") - So(ds.BasicAuth, ShouldBeTrue) - So(ds.BasicAuthUser, ShouldEqual, "basic_auth_user") - So(ds.BasicAuthPassword, ShouldEqual, "basic_auth_password") - So(ds.WithCredentials, ShouldBeTrue) - So(ds.IsDefault, ShouldBeTrue) - So(ds.Editable, ShouldBeTrue) + So(dsCfg.ApiVersion, ShouldEqual, 1) - So(len(ds.JsonData), ShouldBeGreaterThan, 2) - So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1") - So(ds.JsonData["tlsAuth"], ShouldEqual, true) - So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true) + validateDatasource(dsCfg) + validateDeleteDatasources(dsCfg) + }) - So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2) - So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==") - So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==") - So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==") + Convey("can read all properties from version 0", func() { + cfgProvifer := &configReader{log: log.New("test logger")} + cfg, err := cfgProvifer.readConfig(versionZero) + if err != nil { + t.Fatalf("readConfig return an error %v", err) + } - dstwo := cfg[1].Datasources[0] - So(dstwo.Name, ShouldEqual, "name2") + So(len(cfg), ShouldEqual, 1) + + dsCfg := cfg[0] + + So(dsCfg.ApiVersion, ShouldEqual, 0) + + validateDatasource(dsCfg) + validateDeleteDatasources(dsCfg) }) }) } +func validateDeleteDatasources(dsCfg *DatasourcesAsConfig) { + So(len(dsCfg.DeleteDatasources), ShouldEqual, 1) + deleteDs := dsCfg.DeleteDatasources[0] + So(deleteDs.Name, ShouldEqual, "old-graphite3") + So(deleteDs.OrgId, ShouldEqual, 2) +} +func validateDatasource(dsCfg *DatasourcesAsConfig) { + ds := dsCfg.Datasources[0] + So(ds.Name, ShouldEqual, "name") + So(ds.Type, ShouldEqual, "type") + So(ds.Access, ShouldEqual, models.DS_ACCESS_PROXY) + So(ds.OrgId, ShouldEqual, 2) + So(ds.Url, ShouldEqual, "url") + So(ds.User, ShouldEqual, "user") + So(ds.Password, ShouldEqual, "password") + So(ds.Database, ShouldEqual, "database") + So(ds.BasicAuth, ShouldBeTrue) + So(ds.BasicAuthUser, ShouldEqual, "basic_auth_user") + So(ds.BasicAuthPassword, ShouldEqual, "basic_auth_password") + So(ds.WithCredentials, ShouldBeTrue) + So(ds.IsDefault, ShouldBeTrue) + So(ds.Editable, ShouldBeTrue) + So(ds.Version, ShouldEqual, 10) + + So(len(ds.JsonData), ShouldBeGreaterThan, 2) + So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1") + So(ds.JsonData["tlsAuth"], ShouldEqual, true) + So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true) + + So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2) + So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==") + So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==") + So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==") +} type fakeRepository struct { inserted []*models.AddDataSourceCommand diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go index aa1308ffa29..1fa0a3b3173 100644 --- a/pkg/services/provisioning/datasources/datasources.go +++ b/pkg/services/provisioning/datasources/datasources.go @@ -2,16 +2,12 @@ package datasources import ( "errors" - "io/ioutil" - "path/filepath" - "strings" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" - yaml "gopkg.in/yaml.v2" ) var ( @@ -94,65 +90,3 @@ func (dc *DatasourceProvisioner) deleteDatasources(dsToDelete []*DeleteDatasourc return nil } - -type configReader struct { - log log.Logger -} - -func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) { - var datasources []*DatasourcesAsConfig - - files, err := ioutil.ReadDir(path) - if err != nil { - cr.log.Error("cant read datasource provisioning files from directory", "path", path) - return datasources, nil - } - - for _, file := range files { - if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") { - filename, _ := filepath.Abs(filepath.Join(path, file.Name())) - yamlFile, err := ioutil.ReadFile(filename) - - if err != nil { - return nil, err - } - var datasource *DatasourcesAsConfig - err = yaml.Unmarshal(yamlFile, &datasource) - if err != nil { - return nil, err - } - - if datasource != nil { - datasources = append(datasources, datasource) - } - } - } - - defaultCount := 0 - for i := range datasources { - if datasources[i].Datasources == nil { - continue - } - - for _, ds := range datasources[i].Datasources { - if ds.OrgId == 0 { - ds.OrgId = 1 - } - - if ds.IsDefault { - defaultCount++ - if defaultCount > 1 { - return nil, ErrInvalidConfigToManyDefault - } - } - } - - for _, ds := range datasources[i].DeleteDatasources { - if ds.OrgId == 0 { - ds.OrgId = 1 - } - } - } - - return datasources, nil -} diff --git a/pkg/services/provisioning/datasources/test-configs/all-properties/all-properties.yaml b/pkg/services/provisioning/datasources/test-configs/all-properties/all-properties.yaml index af0d3009a4c..b92b81f7079 100644 --- a/pkg/services/provisioning/datasources/test-configs/all-properties/all-properties.yaml +++ b/pkg/services/provisioning/datasources/test-configs/all-properties/all-properties.yaml @@ -1,23 +1,30 @@ +apiVersion: 1 + datasources: - name: name type: type access: proxy - org_id: 2 + orgId: 2 url: url password: password user: user database: database - basic_auth: true - basic_auth_user: basic_auth_user - basic_auth_password: basic_auth_password - with_credentials: true - is_default: true - json_data: + basicAuth: true + basicAuthUser: basic_auth_user + basicAuthPassword: basic_auth_password + withCredentials: true + isDefault: true + jsonData: graphiteVersion: "1.1" tlsAuth: true tlsAuthWithCACert: true - secure_json_data: + secureJsonData: tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==" tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==" tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==" editable: true + version: 10 + +deleteDatasources: + - name: old-graphite3 + orgId: 2 diff --git a/pkg/services/provisioning/datasources/test-configs/all-properties/second.yaml b/pkg/services/provisioning/datasources/test-configs/all-properties/second.yaml index 43c41ee9b3b..9f27a8d07ee 100644 --- a/pkg/services/provisioning/datasources/test-configs/all-properties/second.yaml +++ b/pkg/services/provisioning/datasources/test-configs/all-properties/second.yaml @@ -3,5 +3,5 @@ datasources: - name: name2 type: type2 access: proxy - org_id: 2 + orgId: 2 url: url2 diff --git a/pkg/services/provisioning/datasources/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/datasources/test-configs/version-0/version-0.yaml new file mode 100644 index 00000000000..fcd4ddd6b01 --- /dev/null +++ b/pkg/services/provisioning/datasources/test-configs/version-0/version-0.yaml @@ -0,0 +1,28 @@ +datasources: + - name: name + type: type + access: proxy + org_id: 2 + url: url + password: password + user: user + database: database + basic_auth: true + basic_auth_user: basic_auth_user + basic_auth_password: basic_auth_password + with_credentials: true + is_default: true + json_data: + graphiteVersion: "1.1" + tlsAuth: true + tlsAuthWithCACert: true + secure_json_data: + tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==" + tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==" + tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==" + editable: true + version: 10 + +delete_datasources: + - name: old-graphite3 + org_id: 2 diff --git a/pkg/services/provisioning/datasources/types.go b/pkg/services/provisioning/datasources/types.go index ee2175d6a90..22eae802246 100644 --- a/pkg/services/provisioning/datasources/types.go +++ b/pkg/services/provisioning/datasources/types.go @@ -1,22 +1,74 @@ package datasources -import "github.com/grafana/grafana/pkg/models" +import ( + "github.com/grafana/grafana/pkg/models" +) import "github.com/grafana/grafana/pkg/components/simplejson" +type ConfigVersion struct { + ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"` +} + type DatasourcesAsConfig struct { - Datasources []*DataSourceFromConfig `json:"datasources" yaml:"datasources"` - DeleteDatasources []*DeleteDatasourceConfig `json:"delete_datasources" yaml:"delete_datasources"` + ApiVersion int64 + + Datasources []*DataSourceFromConfig + DeleteDatasources []*DeleteDatasourceConfig } type DeleteDatasourceConfig struct { + OrgId int64 + Name string +} + +type DataSourceFromConfig struct { + OrgId int64 + Version int + + Name string + Type string + Access string + Url string + Password string + User string + Database string + BasicAuth bool + BasicAuthUser string + BasicAuthPassword string + WithCredentials bool + IsDefault bool + JsonData map[string]interface{} + SecureJsonData map[string]string + Editable bool +} + +type DatasourcesAsConfigV0 struct { + ConfigVersion + + Datasources []*DataSourceFromConfigV0 `json:"datasources" yaml:"datasources"` + DeleteDatasources []*DeleteDatasourceConfigV0 `json:"delete_datasources" yaml:"delete_datasources"` +} + +type DatasourcesAsConfigV1 struct { + ConfigVersion + + Datasources []*DataSourceFromConfigV1 `json:"datasources" yaml:"datasources"` + DeleteDatasources []*DeleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"` +} + +type DeleteDatasourceConfigV0 struct { OrgId int64 `json:"org_id" yaml:"org_id"` Name string `json:"name" yaml:"name"` } -type DataSourceFromConfig struct { - OrgId int64 `json:"org_id" yaml:"org_id"` - Version int `json:"version" yaml:"version"` +type DeleteDatasourceConfigV1 struct { + OrgId int64 `json:"orgId" yaml:"orgId"` + Name string `json:"name" yaml:"name"` +} +type DataSourceFromConfigV0 struct { + OrgId int64 `json:"org_id" yaml:"org_id"` + Version int `json:"version" yaml:"version"` Name string `json:"name" yaml:"name"` Type string `json:"type" yaml:"type"` Access string `json:"access" yaml:"access"` @@ -34,6 +86,100 @@ type DataSourceFromConfig struct { Editable bool `json:"editable" yaml:"editable"` } +type DataSourceFromConfigV1 struct { + OrgId int64 `json:"orgId" yaml:"orgId"` + Version int `json:"version" yaml:"version"` + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + Access string `json:"access" yaml:"access"` + Url string `json:"url" yaml:"url"` + Password string `json:"password" yaml:"password"` + User string `json:"user" yaml:"user"` + Database string `json:"database" yaml:"database"` + BasicAuth bool `json:"basicAuth" yaml:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser" yaml:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword" yaml:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials" yaml:"withCredentials"` + IsDefault bool `json:"isDefault" yaml:"isDefault"` + JsonData map[string]interface{} `json:"jsonData" yaml:"jsonData"` + SecureJsonData map[string]string `json:"secureJsonData" yaml:"secureJsonData"` + Editable bool `json:"editable" yaml:"editable"` +} + +func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig { + r := &DatasourcesAsConfig{} + + r.ApiVersion = apiVersion + + for _, ds := range cfg.Datasources { + r.Datasources = append(r.Datasources, &DataSourceFromConfig{ + OrgId: ds.OrgId, + Name: ds.Name, + Type: ds.Type, + Access: ds.Access, + Url: ds.Url, + Password: ds.Password, + User: ds.User, + Database: ds.Database, + BasicAuth: ds.BasicAuth, + BasicAuthUser: ds.BasicAuthUser, + BasicAuthPassword: ds.BasicAuthPassword, + WithCredentials: ds.WithCredentials, + IsDefault: ds.IsDefault, + JsonData: ds.JsonData, + SecureJsonData: ds.SecureJsonData, + Editable: ds.Editable, + Version: ds.Version, + }) + } + + for _, ds := range cfg.DeleteDatasources { + r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{ + OrgId: ds.OrgId, + Name: ds.Name, + }) + } + + return r +} + +func (cfg *DatasourcesAsConfigV0) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig { + r := &DatasourcesAsConfig{} + + r.ApiVersion = apiVersion + + for _, ds := range cfg.Datasources { + r.Datasources = append(r.Datasources, &DataSourceFromConfig{ + OrgId: ds.OrgId, + Name: ds.Name, + Type: ds.Type, + Access: ds.Access, + Url: ds.Url, + Password: ds.Password, + User: ds.User, + Database: ds.Database, + BasicAuth: ds.BasicAuth, + BasicAuthUser: ds.BasicAuthUser, + BasicAuthPassword: ds.BasicAuthPassword, + WithCredentials: ds.WithCredentials, + IsDefault: ds.IsDefault, + JsonData: ds.JsonData, + SecureJsonData: ds.SecureJsonData, + Editable: ds.Editable, + Version: ds.Version, + }) + } + + for _, ds := range cfg.DeleteDatasources { + r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{ + OrgId: ds.OrgId, + Name: ds.Name, + }) + } + + return r +} + func createInsertCommand(ds *DataSourceFromConfig) *models.AddDataSourceCommand { jsonData := simplejson.New() if len(ds.JsonData) > 0 { From b010e4df9388a38b25d6ec8c64cd480526524702 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Feb 2018 14:14:10 +0100 Subject: [PATCH 20/73] provisioning: support camelcase for dashboards configs --- conf/provisioning/dashboards/sample.yaml | 6 +- docs/sources/administration/provisioning.md | 5 +- .../provisioning/dashboards/config_reader.go | 40 ++++++++++-- .../dashboards/config_reader_test.go | 63 ++++++++++--------- .../dashboards-from-disk/dev-dashboards.yaml | 5 +- .../test-configs/version-0/version-0.yaml | 12 ++++ pkg/services/provisioning/dashboards/types.go | 62 ++++++++++++++++++ 7 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml diff --git a/conf/provisioning/dashboards/sample.yaml b/conf/provisioning/dashboards/sample.yaml index f0dcca9b47a..2eebdee9ed5 100644 --- a/conf/provisioning/dashboards/sample.yaml +++ b/conf/provisioning/dashboards/sample.yaml @@ -1,5 +1,9 @@ +# # config file version +# apiVersion: 1 + +#providers: # - name: 'default' -# org_id: 1 +# orgId: 1 # folder: '' # type: file # options: diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index e783798b97d..924a8245509 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -177,8 +177,11 @@ It's possible to manage dashboards in Grafana by adding one or more yaml config The dashboard provider config file looks somewhat like this: ```yaml +apiVersion: 1 + +providers: - name: 'default' - org_id: 1 + orgId: 1 folder: '' type: file options: diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index ab9e85f4d38..ee3cf2085b2 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -14,6 +14,37 @@ type configReader struct { log log.Logger } +func parseConfigs(yamlFile []byte) ([]*DashboardsAsConfig, error) { + apiVersion := &ConfigVersion{ApiVersion: 0} + yaml.Unmarshal(yamlFile, &apiVersion) + + if apiVersion.ApiVersion > 0 { + + v1 := &DashboardAsConfigV1{} + err := yaml.Unmarshal(yamlFile, &v1) + if err != nil { + return nil, err + } + + if v1 != nil { + return v1.mapToDashboardAsConfig(), nil + } + + } else { + var v0 []*DashboardsAsConfigV0 + err := yaml.Unmarshal(yamlFile, &v0) + if err != nil { + return nil, err + } + + if v0 != nil { + return convertv0ToDashboardAsConfig(v0), nil + } + } + + return []*DashboardsAsConfig{}, nil +} + func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { var dashboards []*DashboardsAsConfig @@ -35,13 +66,14 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { return nil, err } - var dashCfg []*DashboardsAsConfig - err = yaml.Unmarshal(yamlFile, &dashCfg) + parsedDashboards, err := parseConfigs(yamlFile) if err != nil { - return nil, err + } - dashboards = append(dashboards, dashCfg...) + if len(parsedDashboards) > 0 { + dashboards = append(dashboards, parsedDashboards...) + } } for i := range dashboards { diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index bb960a72094..95f9b3561ea 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -9,48 +9,33 @@ import ( var ( simpleDashboardConfig string = "./test-configs/dashboards-from-disk" + oldVersion string = "./test-configs/version-0" brokenConfigs string = "./test-configs/broken-configs" ) func TestDashboardsAsConfig(t *testing.T) { Convey("Dashboards as configuration", t, func() { + logger := log.New("test-logger") - Convey("Can read config file", func() { - - cfgProvider := configReader{path: simpleDashboardConfig, log: log.New("test-logger")} + Convey("Can read config file version 1 format", func() { + cfgProvider := configReader{path: simpleDashboardConfig, log: logger} cfg, err := cfgProvider.readConfig() - if err != nil { - t.Fatalf("readConfig return an error %v", err) - } + So(err, ShouldBeNil) - So(len(cfg), ShouldEqual, 2) + validateDashboardAsConfig(cfg) + }) - ds := cfg[0] + Convey("Can read config file in version 0 format", func() { + cfgProvider := configReader{path: oldVersion, log: logger} + cfg, err := cfgProvider.readConfig() + So(err, ShouldBeNil) - So(ds.Name, ShouldEqual, "general dashboards") - So(ds.Type, ShouldEqual, "file") - So(ds.OrgId, ShouldEqual, 2) - So(ds.Folder, ShouldEqual, "developers") - So(ds.Editable, ShouldBeTrue) - - So(len(ds.Options), ShouldEqual, 1) - So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") - - ds2 := cfg[1] - - So(ds2.Name, ShouldEqual, "default") - So(ds2.Type, ShouldEqual, "file") - So(ds2.OrgId, ShouldEqual, 1) - So(ds2.Folder, ShouldEqual, "") - So(ds2.Editable, ShouldBeFalse) - - So(len(ds2.Options), ShouldEqual, 1) - So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") + validateDashboardAsConfig(cfg) }) Convey("Should skip invalid path", func() { - cfgProvider := configReader{path: "/invalid-directory", log: log.New("test-logger")} + cfgProvider := configReader{path: "/invalid-directory", log: logger} cfg, err := cfgProvider.readConfig() if err != nil { t.Fatalf("readConfig return an error %v", err) @@ -61,7 +46,7 @@ func TestDashboardsAsConfig(t *testing.T) { Convey("Should skip broken config files", func() { - cfgProvider := configReader{path: brokenConfigs, log: log.New("test-logger")} + cfgProvider := configReader{path: brokenConfigs, log: logger} cfg, err := cfgProvider.readConfig() if err != nil { t.Fatalf("readConfig return an error %v", err) @@ -71,3 +56,23 @@ func TestDashboardsAsConfig(t *testing.T) { }) }) } +func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { + So(len(cfg), ShouldEqual, 2) + + ds := cfg[0] + So(ds.Name, ShouldEqual, "general dashboards") + So(ds.Type, ShouldEqual, "file") + So(ds.OrgId, ShouldEqual, 2) + So(ds.Folder, ShouldEqual, "developers") + So(ds.Editable, ShouldBeTrue) + So(len(ds.Options), ShouldEqual, 1) + So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") + ds2 := cfg[1] + So(ds2.Name, ShouldEqual, "default") + So(ds2.Type, ShouldEqual, "file") + So(ds2.OrgId, ShouldEqual, 1) + So(ds2.Folder, ShouldEqual, "") + So(ds2.Editable, ShouldBeFalse) + So(len(ds2.Options), ShouldEqual, 1) + So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") +} diff --git a/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml index df0e6ff3044..b55fd303a86 100644 --- a/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml +++ b/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -1,5 +1,8 @@ +apiVersion: 1 + +providers: - name: 'general dashboards' - org_id: 2 + orgId: 2 folder: 'developers' editable: true type: file diff --git a/pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml new file mode 100644 index 00000000000..df0e6ff3044 --- /dev/null +++ b/pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml @@ -0,0 +1,12 @@ +- name: 'general dashboards' + org_id: 2 + folder: 'developers' + editable: true + type: file + options: + path: /var/lib/grafana/dashboards + +- name: 'default' + type: file + options: + path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 91379b33148..160188f81db 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -10,6 +10,15 @@ import ( ) type DashboardsAsConfig struct { + Name string + Type string + OrgId int64 + Folder string + Editable bool + Options map[string]interface{} +} + +type DashboardsAsConfigV0 struct { Name string `json:"name" yaml:"name"` Type string `json:"type" yaml:"type"` OrgId int64 `json:"org_id" yaml:"org_id"` @@ -18,6 +27,59 @@ type DashboardsAsConfig struct { Options map[string]interface{} `json:"options" yaml:"options"` } +func convertv0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig { + var r []*DashboardsAsConfig + + for _, v := range v0 { + r = append(r, &DashboardsAsConfig{ + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + }) + } + + return r +} + +type ConfigVersion struct { + ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"` +} + +type DashboardAsConfigV1 struct { + ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"` + + Providers []*DashboardSource `json:"providers" yaml:"providers"` +} + +func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { + var r []*DashboardsAsConfig + + for _, v := range dc.Providers { + r = append(r, &DashboardsAsConfig{ + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + }) + } + + return r +} + +type DashboardSource struct { + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"orgId" yaml:"orgId"` + Folder string `json:"folder" yaml:"folder"` + Editable bool `json:"editable" yaml:"editable"` + Options map[string]interface{} `json:"options" yaml:"options"` +} + func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) { dash := &dashboards.SaveDashboardDTO{} dash.Dashboard = models.NewDashboardFromJson(data) From 44baaeed8f28eae3c6094878e2867de21a3b5471 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Feb 2018 14:29:56 +0100 Subject: [PATCH 21/73] provisioning: adds logs about deprecated config format --- .../provisioning/dashboards/config_reader.go | 19 ++++++++-------- .../provisioning/datasources/config_reader.go | 22 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index ee3cf2085b2..183c6e2adb2 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -2,6 +2,7 @@ package dashboards import ( "io/ioutil" + "os" "path/filepath" "strings" @@ -14,7 +15,13 @@ type configReader struct { log log.Logger } -func parseConfigs(yamlFile []byte) ([]*DashboardsAsConfig, error) { +func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, error) { + filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name())) + yamlFile, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + apiVersion := &ConfigVersion{ApiVersion: 0} yaml.Unmarshal(yamlFile, &apiVersion) @@ -38,6 +45,7 @@ func parseConfigs(yamlFile []byte) ([]*DashboardsAsConfig, error) { } if v0 != nil { + cr.log.Warn("[Deprecated] the dashboard provisioning config is outdated. please upgrade", "filename", filename) return convertv0ToDashboardAsConfig(v0), nil } } @@ -49,7 +57,6 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { var dashboards []*DashboardsAsConfig files, err := ioutil.ReadDir(cr.path) - if err != nil { cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path) return dashboards, nil @@ -60,13 +67,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { continue } - filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name())) - yamlFile, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - parsedDashboards, err := parseConfigs(yamlFile) + parsedDashboards, err := cr.parseConfigs(file) if err != nil { } diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 434b487bd2d..de1aec74b22 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -2,6 +2,7 @@ package datasources import ( "io/ioutil" + "os" "path/filepath" "strings" @@ -24,13 +25,7 @@ func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) for _, file := range files { if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") { - filename, _ := filepath.Abs(filepath.Join(path, file.Name())) - yamlFile, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - datasource, err := parseDatasourceConfig(yamlFile) + datasource, err := cr.parseDatasourceConfig(path, file) if err != nil { return nil, err } @@ -49,7 +44,13 @@ func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) return datasources, nil } -func parseDatasourceConfig(yamlFile []byte) (*DatasourcesAsConfig, error) { +func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*DatasourcesAsConfig, error) { + filename, _ := filepath.Abs(filepath.Join(path, file.Name())) + yamlFile, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + var apiVersion *ConfigVersion err := yaml.Unmarshal(yamlFile, &apiVersion) if err != nil { @@ -62,8 +63,8 @@ func parseDatasourceConfig(yamlFile []byte) (*DatasourcesAsConfig, error) { if err != nil { return nil, err } - return v1.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil + return v1.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil } var v0 *DatasourcesAsConfigV0 @@ -71,6 +72,9 @@ func parseDatasourceConfig(yamlFile []byte) (*DatasourcesAsConfig, error) { if err != nil { return nil, err } + + cr.log.Warn("[Deprecated] the datasource provisioning config is outdated. please upgrade", "filename", filename) + return v0.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil } From 84de76ff0a08f7de97836a5b93db29eeb7832d63 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Feb 2018 15:02:27 +0100 Subject: [PATCH 22/73] provisioning: code formating --- .../provisioning/dashboards/config_reader.go | 2 +- pkg/services/provisioning/dashboards/types.go | 74 +++++++++---------- .../provisioning/datasources/config_reader.go | 2 +- 3 files changed, 38 insertions(+), 40 deletions(-) diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 183c6e2adb2..9030ba609b9 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -46,7 +46,7 @@ func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, e if v0 != nil { cr.log.Warn("[Deprecated] the dashboard provisioning config is outdated. please upgrade", "filename", filename) - return convertv0ToDashboardAsConfig(v0), nil + return mapV0ToDashboardAsConfig(v0), nil } } diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 160188f81db..0fdc7b0c3ca 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -27,51 +27,15 @@ type DashboardsAsConfigV0 struct { Options map[string]interface{} `json:"options" yaml:"options"` } -func convertv0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig { - var r []*DashboardsAsConfig - - for _, v := range v0 { - r = append(r, &DashboardsAsConfig{ - Name: v.Name, - Type: v.Type, - OrgId: v.OrgId, - Folder: v.Folder, - Editable: v.Editable, - Options: v.Options, - }) - } - - return r -} - type ConfigVersion struct { ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"` } type DashboardAsConfigV1 struct { - ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"` - - Providers []*DashboardSource `json:"providers" yaml:"providers"` + Providers []*DashboardProviderConfigs `json:"providers" yaml:"providers"` } -func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { - var r []*DashboardsAsConfig - - for _, v := range dc.Providers { - r = append(r, &DashboardsAsConfig{ - Name: v.Name, - Type: v.Type, - OrgId: v.OrgId, - Folder: v.Folder, - Editable: v.Editable, - Options: v.Options, - }) - } - - return r -} - -type DashboardSource struct { +type DashboardProviderConfigs struct { Name string `json:"name" yaml:"name"` Type string `json:"type" yaml:"type"` OrgId int64 `json:"orgId" yaml:"orgId"` @@ -98,3 +62,37 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das return dash, nil } + +func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig { + var r []*DashboardsAsConfig + + for _, v := range v0 { + r = append(r, &DashboardsAsConfig{ + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + }) + } + + return r +} + +func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { + var r []*DashboardsAsConfig + + for _, v := range dc.Providers { + r = append(r, &DashboardsAsConfig{ + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + }) + } + + return r +} diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index de1aec74b22..772436987c3 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -52,7 +52,7 @@ func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*D } var apiVersion *ConfigVersion - err := yaml.Unmarshal(yamlFile, &apiVersion) + err = yaml.Unmarshal(yamlFile, &apiVersion) if err != nil { return nil, err } From fc371af47f2cd77954b567108592e6d10145358e Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Feb 2018 10:33:02 +0100 Subject: [PATCH 23/73] adds tests that validate that updated is correct --- pkg/services/sqlstore/dashboard_provisioning_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index c42d95e7495..0ec55f91bd5 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -2,6 +2,7 @@ package sqlstore import ( "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" @@ -23,11 +24,14 @@ func TestDashboardProvisioningTest(t *testing.T) { } Convey("Saving dashboards with extras", func() { + now := time.Now() + cmd := &models.SaveProvisionedDashboardCommand{ DashboardCmd: saveDashboardCmd, DashboardProvisioning: &models.DashboardProvisioning{ Name: "default", ExternalId: "/var/grafana.json", + Updated: now, }, } @@ -44,6 +48,7 @@ func TestDashboardProvisioningTest(t *testing.T) { So(len(query.Result), ShouldEqual, 1) So(query.Result[0].DashboardId, ShouldEqual, dashId) + So(query.Result[0].Updated, ShouldEqual, now) }) }) }) From b60b6690ba55ae3d902297687c431a4b9c155d6c Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 13 Feb 2018 16:51:11 +0100 Subject: [PATCH 24/73] sql: removes locale from test to mirror prod. --- pkg/services/sqlstore/dashboard_provisioning_test.go | 2 +- pkg/services/sqlstore/sqlutil/sqlutil.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 0ec55f91bd5..8b2ed7ff061 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -48,7 +48,7 @@ func TestDashboardProvisioningTest(t *testing.T) { So(len(query.Result), ShouldEqual, 1) So(query.Result[0].DashboardId, ShouldEqual, dashId) - So(query.Result[0].Updated, ShouldEqual, now) + So(query.Result[0].Updated.Unix(), ShouldEqual, now.Unix()) }) }) }) diff --git a/pkg/services/sqlstore/sqlutil/sqlutil.go b/pkg/services/sqlstore/sqlutil/sqlutil.go index a33872ed687..26a58811e0f 100644 --- a/pkg/services/sqlstore/sqlutil/sqlutil.go +++ b/pkg/services/sqlstore/sqlutil/sqlutil.go @@ -11,8 +11,8 @@ type TestDB struct { ConnStr string } -var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"} -var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci&loc=Local"} +var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:"} +var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"} var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"} func CleanDB(x *xorm.Engine) { From 165304a342ee66f1538fa22e06654d38f403f31e Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Feb 2018 11:22:24 +0100 Subject: [PATCH 25/73] provisioning: handle nil configs --- pkg/services/provisioning/datasources/config_reader.go | 4 ++++ pkg/services/provisioning/datasources/types.go | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 772436987c3..58ed5472a6b 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -57,6 +57,10 @@ func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*D return nil, err } + if apiVersion == nil { + apiVersion = &ConfigVersion{ApiVersion: 0} + } + if apiVersion.ApiVersion > 0 { var v1 *DatasourcesAsConfigV1 err = yaml.Unmarshal(yamlFile, &v1) diff --git a/pkg/services/provisioning/datasources/types.go b/pkg/services/provisioning/datasources/types.go index 22eae802246..8e2443a0169 100644 --- a/pkg/services/provisioning/datasources/types.go +++ b/pkg/services/provisioning/datasources/types.go @@ -111,6 +111,10 @@ func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *D r.ApiVersion = apiVersion + if cfg == nil { + return r + } + for _, ds := range cfg.Datasources { r.Datasources = append(r.Datasources, &DataSourceFromConfig{ OrgId: ds.OrgId, @@ -148,6 +152,10 @@ func (cfg *DatasourcesAsConfigV0) mapToDatasourceFromConfig(apiVersion int64) *D r.ApiVersion = apiVersion + if cfg == nil { + return r + } + for _, ds := range cfg.Datasources { r.Datasources = append(r.Datasources, &DataSourceFromConfig{ OrgId: ds.OrgId, From dba087463aec54c058407f84f90d79b1fc663da0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Feb 2018 11:33:58 +0100 Subject: [PATCH 26/73] provisioing: always skip sample.yaml files --- conf/provisioning/dashboards/sample.yaml | 3 ++ conf/provisioning/datasources/sample.yaml | 3 ++ .../provisioning/dashboards/config_reader.go | 2 +- .../dashboards-from-disk/sample.yaml | 10 ++++++ .../provisioning/datasources/config_reader.go | 2 +- .../test-configs/all-properties/sample.yaml | 32 +++++++++++++++++++ 6 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/sample.yaml create mode 100644 pkg/services/provisioning/datasources/test-configs/all-properties/sample.yaml diff --git a/conf/provisioning/dashboards/sample.yaml b/conf/provisioning/dashboards/sample.yaml index 2eebdee9ed5..caaf3754b0f 100644 --- a/conf/provisioning/dashboards/sample.yaml +++ b/conf/provisioning/dashboards/sample.yaml @@ -1,3 +1,6 @@ +# This file is only an example. +# Grafana will never read sample.yaml files + # # config file version # apiVersion: 1 diff --git a/conf/provisioning/datasources/sample.yaml b/conf/provisioning/datasources/sample.yaml index cffeb3e0d2d..740b4c19772 100644 --- a/conf/provisioning/datasources/sample.yaml +++ b/conf/provisioning/datasources/sample.yaml @@ -1,3 +1,6 @@ +# This file is only an example. +# Grafana will never read sample.yaml files + # # config file version # apiVersion: 1 diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 9030ba609b9..3183d21262a 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -63,7 +63,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { } for _, file := range files { - if !strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml") { + if (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml")) || file.Name() == "sample.yaml" { continue } diff --git a/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/sample.yaml b/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/sample.yaml new file mode 100644 index 00000000000..9090e5f472a --- /dev/null +++ b/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/sample.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 + +providers: +- name: 'gasdf' + orgId: 2 + folder: 'developers' + editable: true + type: file + options: + path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 58ed5472a6b..82504f4972b 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -24,7 +24,7 @@ func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) } for _, file := range files { - if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") { + if (strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml")) && file.Name() != "sample.yaml" { datasource, err := cr.parseDatasourceConfig(path, file) if err != nil { return nil, err diff --git a/pkg/services/provisioning/datasources/test-configs/all-properties/sample.yaml b/pkg/services/provisioning/datasources/test-configs/all-properties/sample.yaml new file mode 100644 index 00000000000..70ad6c6d2f6 --- /dev/null +++ b/pkg/services/provisioning/datasources/test-configs/all-properties/sample.yaml @@ -0,0 +1,32 @@ +# Should not be included + + +apiVersion: 1 + +datasources: + - name: name + type: type + access: proxy + orgId: 2 + url: url + password: password + user: user + database: database + basicAuth: true + basicAuthUser: basic_auth_user + basicAuthPassword: basic_auth_password + withCredentials: true + jsonData: + graphiteVersion: "1.1" + tlsAuth: true + tlsAuthWithCACert: true + secureJsonData: + tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==" + tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==" + tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==" + editable: true + version: 10 + +deleteDatasources: + - name: old-graphite3 + orgId: 2 From 1a041a2250d6dedbcce3bcd391bb797159c1e98e Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Feb 2018 13:32:52 +0100 Subject: [PATCH 27/73] bug: return correct err message if the sql query failed has is false and the method will return m.ErrDataSourceNotFound which is incorrect. We now return the correct error message from the query ref #10843 --- pkg/services/sqlstore/datasource.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index e9b400a1772..00d520bcfc6 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -27,6 +27,9 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error { datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id} has, err := x.Get(&datasource) + if err != nil { + return err + } if !has { return m.ErrDataSourceNotFound From fcaa8227a6d914ca2dbbcffedf7ce4c63d968264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Feb 2018 15:04:26 +0100 Subject: [PATCH 28/73] Dashboard acl query fixes (#10909) * initial fixes for dashboard permission acl list query, fixes #10864 * permissions: refactoring of acl api and query --- pkg/api/api.go | 1 - pkg/api/dashboard_acl.go | 30 ---- pkg/api/dashboard_acl_test.go | 104 ++----------- pkg/api/dashboard_test.go | 8 +- pkg/models/dashboard_acl.go | 16 -- pkg/services/guardian/guardian.go | 20 --- pkg/services/sqlstore/dashboard.go | 32 +--- pkg/services/sqlstore/dashboard_acl.go | 147 +++--------------- pkg/services/sqlstore/dashboard_acl_test.go | 91 ++++------- .../sqlstore/dashboard_folder_test.go | 29 ++-- pkg/services/sqlstore/dashboard_test.go | 19 --- pkg/services/sqlstore/org_test.go | 15 +- pkg/services/sqlstore/team_test.go | 2 +- pkg/services/sqlstore/user_test.go | 2 +- .../PermissionsStore/PermissionsStoreItem.ts | 3 +- 15 files changed, 89 insertions(+), 430 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index c03bf7963b8..1320663f630 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -269,7 +269,6 @@ func (hs *HttpServer) registerRoutes() { dashIdRoute.Group("/acl", func(aclRoute RouteRegister) { aclRoute.Get("/", wrap(GetDashboardAclList)) aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl)) - aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl)) }) }) }) diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go index 45f121dd0d0..32b75e80cc0 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_acl.go @@ -84,33 +84,3 @@ func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCom return ApiSuccess("Dashboard acl updated") } - -func DeleteDashboardAcl(c *middleware.Context) Response { - dashId := c.ParamsInt64(":dashboardId") - aclId := c.ParamsInt64(":aclId") - - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") - if rsp != nil { - return rsp - } - - guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) - if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { - return dashboardGuardianResponse(err) - } - - if okToDelete, err := guardian.CheckPermissionBeforeRemove(m.PERMISSION_ADMIN, aclId); err != nil || !okToDelete { - if err != nil { - return ApiError(500, "Error while checking dashboard permissions", err) - } - - return ApiError(403, "Cannot remove own admin permission for a folder", nil) - } - - cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId} - if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to delete permission for user", err) - } - - return Json(200, "") -} diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_acl_test.go index e43e57ed5c0..d6b7e305daf 100644 --- a/pkg/api/dashboard_acl_test.go +++ b/pkg/api/dashboard_acl_test.go @@ -15,11 +15,11 @@ import ( func TestDashboardAclApiEndpoint(t *testing.T) { Convey("Given a dashboard acl", t, func() { mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW}, - {Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, - {Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, - {Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, - {Id: 5, OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, + {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, } dtoRes := transformDashboardAclsToDTOs(mockResult) @@ -92,21 +92,11 @@ func TestDashboardAclApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 404) }) }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/2/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_ADMIN, func(sc *scenarioContext) { - getDashboardNotFoundError = m.ErrDashboardNotFound - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - Convey("Should not be able to delete non-existing dashboard", func() { - So(sc.resp.Code, ShouldEqual, 404) - }) - }) }) Convey("When user is org editor and has admin permission in the ACL", func() { loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) + mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) Convey("Should be able to access ACL", func() { sc.handlerFunc = GetDashboardAclList @@ -116,36 +106,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) - - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should be able to delete permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 200) - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) - - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should not be able to delete their own Admin permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 403) - }) - }) - Convey("Should not be able to downgrade their own Admin permission", func() { cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -154,7 +114,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { } postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) + mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) CallPostAcl(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -170,34 +130,18 @@ func TestDashboardAclApiEndpoint(t *testing.T) { } postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) + mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) CallPostAcl(sc) So(sc.resp.Code, ShouldEqual, 200) }) }) - Convey("When user is a member of a team in the ACL with admin permission", func() { - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { - teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"}) - - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should be able to delete permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 200) - }) - }) - }) }) Convey("When user is org viewer and has edit permission in the ACL", func() { loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_VIEWER, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) + mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) // Getting the permissions is an Admin permission Convey("Should not be able to get list of permissions from ACL", func() { @@ -207,21 +151,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_VIEWER, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) - - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should be not be able to delete permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 403) - }) - }) }) Convey("When user is org editor and not in the ACL", func() { @@ -234,20 +163,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW}) - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should be not be able to delete permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 403) - }) - }) }) }) } @@ -257,7 +172,6 @@ func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardA for _, acl := range acls { dto := &m.DashboardAclInfoDTO{ - Id: acl.Id, OrgId: acl.OrgId, DashboardId: acl.DashboardId, Permission: acl.Permission, diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e80b3cad4dc..4a45c561d57 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -431,7 +431,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_VIEWER mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT}, + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT}, } bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { @@ -505,7 +505,7 @@ func TestDashboardApiEndpoint(t *testing.T) { setting.ViewersCanEdit = true mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, } bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { @@ -564,7 +564,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_VIEWER mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN}, } bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { @@ -637,7 +637,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_EDITOR mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, } bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 933487650e3..202b519207d 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -44,7 +44,6 @@ type DashboardAcl struct { } type DashboardAclInfoDTO struct { - Id int64 `json:"id"` OrgId int64 `json:"-"` DashboardId int64 `json:"dashboardId"` @@ -75,21 +74,6 @@ type UpdateDashboardAclCommand struct { Items []*DashboardAcl } -type SetDashboardAclCommand struct { - DashboardId int64 - OrgId int64 - UserId int64 - TeamId int64 - Permission PermissionType - - Result DashboardAcl -} - -type RemoveDashboardAclCommand struct { - AclId int64 - OrgId int64 -} - // // QUERIES // diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index b448561494d..05795b7f2df 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -106,26 +106,6 @@ func (g *DashboardGuardian) checkAcl(permission m.PermissionType, acl []*m.Dashb return false, nil } -func (g *DashboardGuardian) CheckPermissionBeforeRemove(permission m.PermissionType, aclIdToRemove int64) (bool, error) { - if g.user.OrgRole == m.ROLE_ADMIN { - return true, nil - } - - acl, err := g.GetAcl() - if err != nil { - return false, err - } - - for i, p := range acl { - if p.Id == aclIdToRemove { - acl = append(acl[:i], acl[i+1:]...) - break - } - } - - return g.checkAcl(permission, acl) -} - func (g *DashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) { if g.user.OrgRole == m.ROLE_ADMIN { return true, nil diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index f3fd81ebbe2..42c83da8810 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -79,11 +79,6 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { dash.Data.Set("uid", uid) } - err = setHasAcl(sess, dash) - if err != nil { - return err - } - parentVersion := dash.Version affectedRows := int64(0) @@ -100,7 +95,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { dash.Updated = cmd.UpdatedAt } - affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash) + affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash) } if err != nil { @@ -233,31 +228,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { return "", m.ErrDashboardFailedGenerateUniqueUid } -func setHasAcl(sess *DBSession, dash *m.Dashboard) error { - // check if parent has acl - if dash.FolderId > 0 { - var parent m.Dashboard - if hasParent, err := sess.Where("folder_id=?", dash.FolderId).Get(&parent); err != nil { - return err - } else if hasParent && parent.HasAcl { - dash.HasAcl = true - } - } - - // check if dash has its own acl - if dash.Id > 0 { - if res, err := sess.Query("SELECT 1 from dashboard_acl WHERE dashboard_id =?", dash.Id); err != nil { - return err - } else { - if len(res) > 0 { - dash.HasAcl = true - } - } - } - - return nil -} - func GetDashboard(query *m.GetDashboardQuery) error { dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid} has, err := x.Get(&dashboard) diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 829182a8195..ae91d1d41f3 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -1,17 +1,12 @@ package sqlstore import ( - "fmt" - "time" - "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) func init() { - bus.AddHandler("sql", SetDashboardAcl) bus.AddHandler("sql", UpdateDashboardAcl) - bus.AddHandler("sql", RemoveDashboardAcl) bus.AddHandler("sql", GetDashboardAclInfoList) } @@ -24,7 +19,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { } for _, item := range cmd.Items { - if item.UserId == 0 && item.TeamId == 0 && !item.Role.IsValid() { + if item.UserId == 0 && item.TeamId == 0 && (item.Role == nil || !item.Role.IsValid()) { return m.ErrDashboardAclInfoMissing } @@ -40,92 +35,13 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { // Update dashboard HasAcl flag dashboard := m.Dashboard{HasAcl: true} - if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil { + if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil { return err } return nil }) } -func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { - return inTransaction(func(sess *DBSession) error { - if cmd.UserId == 0 && cmd.TeamId == 0 { - return m.ErrDashboardAclInfoMissing - } - - if cmd.DashboardId == 0 { - return m.ErrDashboardPermissionDashboardEmpty - } - - if res, err := sess.Query("SELECT 1 from "+dialect.Quote("dashboard_acl")+" WHERE dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId); err != nil { - return err - } else if len(res) == 1 { - - entity := m.DashboardAcl{ - Permission: cmd.Permission, - Updated: time.Now(), - } - - if _, err := sess.Cols("updated", "permission").Where("dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId).Update(&entity); err != nil { - return err - } - - return nil - } - - entity := m.DashboardAcl{ - OrgId: cmd.OrgId, - TeamId: cmd.TeamId, - UserId: cmd.UserId, - Created: time.Now(), - Updated: time.Now(), - DashboardId: cmd.DashboardId, - Permission: cmd.Permission, - } - - cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"} - - if cmd.UserId != 0 { - cols = append(cols, "user_id") - } - - if cmd.TeamId != 0 { - cols = append(cols, "team_id") - } - - _, err := sess.Cols(cols...).Insert(&entity) - if err != nil { - return err - } - - cmd.Result = entity - - // Update dashboard HasAcl flag - dashboard := m.Dashboard{ - HasAcl: true, - } - - if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil { - return err - } - - return nil - }) -} - -// RemoveDashboardAcl removes a specified permission from the dashboard acl -func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error { - return inTransaction(func(sess *DBSession) error { - var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?" - _, err := sess.Exec(rawSQL, cmd.OrgId, cmd.AclId) - if err != nil { - return err - } - - return err - }) -} - // GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three // different places. // 1) Permissions for the dashboard @@ -134,6 +50,8 @@ func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error { func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { var err error + falseStr := dialect.BooleanStr(false) + if query.DashboardId == 0 { sql := `SELECT da.id, @@ -151,18 +69,13 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { '' as title, '' as slug, '' as uid,` + - dialect.BooleanStr(false) + ` AS is_folder + falseStr + ` AS is_folder FROM dashboard_acl as da WHERE da.dashboard_id = -1` query.Result = make([]*m.DashboardAclInfoDTO, 0) err = x.SQL(sql).Find(&query.Result) } else { - dashboardFilter := fmt.Sprintf(`IN ( - SELECT %d - UNION - SELECT folder_id from dashboard where id = %d - )`, query.DashboardId, query.DashboardId) rawSQL := ` -- get permissions for the dashboard and its parent folder @@ -183,41 +96,21 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { d.slug, d.uid, d.is_folder - FROM` + dialect.Quote("dashboard_acl") + ` as da - LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id - LEFT OUTER JOIN team ug on ug.id = da.team_id - LEFT OUTER JOIN dashboard d on da.dashboard_id = d.id - WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ? - - -- Also include default permissions if folder or dashboard field "has_acl" is false - - UNION - SELECT - da.id, - da.org_id, - da.dashboard_id, - da.user_id, - da.team_id, - da.permission, - da.role, - da.created, - da.updated, - '' as user_login, - '' as user_email, - '' as team, - folder.title, - folder.slug, - folder.uid, - folder.is_folder - FROM dashboard_acl as da, - dashboard as dash - LEFT OUTER JOIN dashboard folder on dash.folder_id = folder.id - WHERE - dash.id = ? AND ( - dash.has_acl = ` + dialect.BooleanStr(false) + ` or - folder.has_acl = ` + dialect.BooleanStr(false) + ` - ) AND - da.dashboard_id = -1 + FROM dashboard as d + LEFT JOIN dashboard folder on folder.id = d.folder_id + LEFT JOIN dashboard_acl AS da ON + da.dashboard_id = d.id OR + da.dashboard_id = d.folder_id OR + ( + -- include default permissions --> + da.org_id = -1 AND ( + (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + ) + ) + LEFT JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id + LEFT JOIN team ug on ug.id = da.team_id + WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL ORDER BY 1 ASC ` diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index 8b712c73ece..8fbb9c0d813 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -17,7 +17,7 @@ func TestDashboardAclDataAccess(t *testing.T) { childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp") Convey("When adding dashboard permission with userId and teamId set to 0", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, DashboardId: savedFolder.Id, Permission: m.PERMISSION_EDIT, @@ -41,8 +41,25 @@ func TestDashboardAclDataAccess(t *testing.T) { }) }) + Convey("Given dashboard folder with removed default permissions", func() { + err := UpdateDashboardAcl(&m.UpdateDashboardAclCommand{ + DashboardId: savedFolder.Id, + Items: []*m.DashboardAcl{}, + }) + So(err, ShouldBeNil) + + Convey("When reading dashboard acl should return no acl items", func() { + query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1} + + err := GetDashboardAclInfoList(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 0) + }) + }) + Convey("Given dashboard folder permission", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, UserId: currentUser.Id, DashboardId: savedFolder.Id, @@ -61,7 +78,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given child dashboard permission", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{ OrgId: 1, UserId: currentUser.Id, DashboardId: childDash.Id, @@ -83,7 +100,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given child dashboard permission in folder with no permissions", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{ OrgId: 1, UserId: currentUser.Id, DashboardId: childDash.Id, @@ -108,17 +125,12 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Should be able to add dashboard permission", func() { - setDashAclCmd := m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, UserId: currentUser.Id, DashboardId: savedFolder.Id, Permission: m.PERMISSION_EDIT, - } - - err := SetDashboardAcl(&setDashAclCmd) - So(err, ShouldBeNil) - - So(setDashAclCmd.Result.Id, ShouldEqual, 3) + }) q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} err = GetDashboardAclInfoList(q1) @@ -130,42 +142,9 @@ func TestDashboardAclDataAccess(t *testing.T) { So(q1.Result[0].UserId, ShouldEqual, currentUser.Id) So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login) So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email) - So(q1.Result[0].Id, ShouldEqual, setDashAclCmd.Result.Id) - - Convey("Should update hasAcl field to true for dashboard folder and its children", func() { - q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}} - err := GetDashboards(q2) - So(err, ShouldBeNil) - So(q2.Result[0].HasAcl, ShouldBeTrue) - So(q2.Result[1].HasAcl, ShouldBeTrue) - }) - - Convey("Should be able to update an existing permission", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ - OrgId: 1, - UserId: 1, - DashboardId: savedFolder.Id, - Permission: m.PERMISSION_ADMIN, - }) - - So(err, ShouldBeNil) - - q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} - err = GetDashboardAclInfoList(q3) - So(err, ShouldBeNil) - So(len(q3.Result), ShouldEqual, 1) - So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id) - So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN) - So(q3.Result[0].UserId, ShouldEqual, 1) - - }) Convey("Should be able to delete an existing permission", func() { - err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{ - OrgId: 1, - AclId: setDashAclCmd.Result.Id, - }) - + err := testHelperUpdateDashboardAcl(savedFolder.Id) So(err, ShouldBeNil) q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} @@ -181,14 +160,12 @@ func TestDashboardAclDataAccess(t *testing.T) { So(err, ShouldBeNil) Convey("Should be able to add a user permission for a team", func() { - setDashAclCmd := m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, TeamId: group1.Result.Id, DashboardId: savedFolder.Id, Permission: m.PERMISSION_EDIT, - } - - err := SetDashboardAcl(&setDashAclCmd) + }) So(err, ShouldBeNil) q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} @@ -197,23 +174,10 @@ func TestDashboardAclDataAccess(t *testing.T) { So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT) So(q1.Result[0].TeamId, ShouldEqual, group1.Result.Id) - - Convey("Should be able to delete an existing permission for a team", func() { - err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{ - OrgId: 1, - AclId: setDashAclCmd.Result.Id, - }) - - So(err, ShouldBeNil) - q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} - err = GetDashboardAclInfoList(q3) - So(err, ShouldBeNil) - So(len(q3.Result), ShouldEqual, 0) - }) }) Convey("Should be able to update an existing permission for a team", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, TeamId: group1.Result.Id, DashboardId: savedFolder.Id, @@ -229,7 +193,6 @@ func TestDashboardAclDataAccess(t *testing.T) { So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN) So(q3.Result[0].TeamId, ShouldEqual, group1.Result.Id) }) - }) }) diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index b32a4dfed1d..40d6cf5bcb2 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -41,7 +41,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and acl is set for dashboard folder", func() { var otherUser int64 = 999 - updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT}) Convey("should not return folder", func() { query := &search.FindPersistedDashboardsQuery{ @@ -55,7 +55,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("when the user is given permission", func() { - updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: currentUser.Id, Permission: m.PERMISSION_EDIT}) Convey("should be able to access folder", func() { query := &search.FindPersistedDashboardsQuery{ @@ -93,9 +93,8 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and acl is set for dashboard child and folder has all permissions removed", func() { var otherUser int64 = 999 - aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) - removeAcl(aclId) - updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder.Id) + testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT}) Convey("should not return folder or child", func() { query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} @@ -106,7 +105,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("when the user is given permission to child", func() { - updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: childDash.Id, OrgId: 1, UserId: currentUser.Id, Permission: m.PERMISSION_EDIT}) Convey("should be able to search for child dashboard but not folder", func() { query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} @@ -165,11 +164,10 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and acl is set for one dashboard folder", func() { var otherUser int64 = 999 - updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT}) Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() { - movedDash := moveDashboard(1, childDash2.Data, folder1.Id) - So(movedDash.HasAcl, ShouldBeTrue) + moveDashboard(1, childDash2.Data, folder1.Id) Convey("should not return folder with acl or its children", func() { query := &search.FindPersistedDashboardsQuery{ @@ -184,9 +182,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) }) Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() { - - movedDash := moveDashboard(1, childDash1.Data, folder2.Id) - So(movedDash.HasAcl, ShouldBeFalse) + moveDashboard(1, childDash1.Data, folder2.Id) Convey("should return folder without acl and its children", func() { query := &search.FindPersistedDashboardsQuery{ @@ -205,9 +201,8 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("and a dashboard with an acl is moved to the folder without an acl", func() { - updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT) - movedDash := moveDashboard(1, childDash1.Data, folder2.Id) - So(movedDash.HasAcl, ShouldBeTrue) + testHelperUpdateDashboardAcl(childDash1.Id, m.DashboardAcl{DashboardId: childDash1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT}) + moveDashboard(1, childDash1.Data, folder2.Id) Convey("should return folder without acl but not the dashboard with acl", func() { query := &search.FindPersistedDashboardsQuery{ @@ -308,7 +303,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() { - updateTestDashboardWithAcl(folder1.Id, editorUser.Id, m.PERMISSION_VIEW) + testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: editorUser.Id, Permission: m.PERMISSION_VIEW}) err := SearchDashboards(&query) So(err, ShouldBeNil) @@ -352,7 +347,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() { - updateTestDashboardWithAcl(folder1.Id, viewerUser.Id, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_EDIT}) err := SearchDashboards(&query) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index de7cdf19927..7de4c5f5701 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -663,25 +663,6 @@ func createUser(name string, role string, isAdmin bool) m.User { return currentUserCmd.Result } -func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.PermissionType) int64 { - cmd := &m.SetDashboardAclCommand{ - OrgId: 1, - UserId: userId, - DashboardId: dashId, - Permission: permissions, - } - - err := SetDashboardAcl(cmd) - So(err, ShouldBeNil) - - return cmd.Result.Id -} - -func removeAcl(aclId int64) { - err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{AclId: aclId, OrgId: 1}) - So(err, ShouldBeNil) -} - func moveDashboard(orgId int64, dashboard *simplejson.Json, newFolderId int64) *m.Dashboard { cmd := m.SaveDashboardCommand{ OrgId: orgId, diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 5322dfd4748..c57d15a48d5 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -199,10 +199,13 @@ func TestAccountDataAccess(t *testing.T) { So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 3) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) + dash1 := insertTestDashboard("1 test dash", ac1.OrgId, 0, false, "prod", "webapp") + dash2 := insertTestDashboard("2 test dash", ac3.OrgId, 0, false, "prod", "webapp") + + err = testHelperUpdateDashboardAcl(dash1.Id, m.DashboardAcl{DashboardId: dash1.Id, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) So(err, ShouldBeNil) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 2, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) + err = testHelperUpdateDashboardAcl(dash2.Id, m.DashboardAcl{DashboardId: dash2.Id, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) So(err, ShouldBeNil) Convey("When org user is deleted", func() { @@ -234,3 +237,11 @@ func TestAccountDataAccess(t *testing.T) { }) }) } + +func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error { + cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId} + for _, item := range items { + cmd.Items = append(cmd.Items, &item) + } + return UpdateDashboardAcl(&cmd) +} diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index bebe59f4238..fb76c3fa9d6 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -99,7 +99,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]}) So(err, ShouldBeNil) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId}) + err = testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId}) err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId}) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index a65b7226eb6..2830733c96a 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -99,7 +99,7 @@ func TestUserDataAccess(t *testing.T) { err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId}) So(err, ShouldBeNil) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT}) + testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT}) So(err, ShouldBeNil) err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"}) diff --git a/public/app/stores/PermissionsStore/PermissionsStoreItem.ts b/public/app/stores/PermissionsStore/PermissionsStoreItem.ts index 74769891256..92dca0220ca 100644 --- a/public/app/stores/PermissionsStore/PermissionsStoreItem.ts +++ b/public/app/stores/PermissionsStore/PermissionsStoreItem.ts @@ -1,9 +1,8 @@ -import { types } from 'mobx-state-tree'; +import { types } from 'mobx-state-tree'; export const PermissionsStoreItem = types .model('PermissionsStoreItem', { dashboardId: types.optional(types.number, -1), - id: types.maybe(types.number), permission: types.number, permissionName: types.maybe(types.string), role: types.maybe(types.string), From 60b30d3e9a6f9d911ed13267c743120a93b5fe49 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 14 Feb 2018 23:14:38 +0900 Subject: [PATCH 29/73] add AWS/States Rekognition (#10890) * added AWS/States * added Rekognition --- pkg/tsdb/cloudwatch/metric_find_query.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 251527ab4e5..c82cff390c3 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -98,11 +98,13 @@ func init() { "AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send"}, "AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"}, "AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateAgeOfOldestMessage", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"}, + "AWS/States": {"ExecutionTime", "ExecutionThrottled", "ExecutionsAborted", "ExecutionsFailed", "ExecutionsStarted", "ExecutionsSucceeded", "ExecutionsTimedOut", "ActivityRunTime", "ActivityScheduleTime", "ActivityTime", "ActivitiesFailed", "ActivitiesHeartbeatTimedOut", "ActivitiesScheduled", "ActivitiesScheduled", "ActivitiesSucceeded", "ActivitiesTimedOut", "LambdaFunctionRunTime", "LambdaFunctionScheduleTime", "LambdaFunctionTime", "LambdaFunctionsFailed", "LambdaFunctionsHeartbeatTimedOut", "LambdaFunctionsScheduled", "LambdaFunctionsStarted", "LambdaFunctionsSucceeded", "LambdaFunctionsTimedOut"}, "AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed", "CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"}, "AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut", "ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"}, "AWS/VPN": {"TunnelState", "TunnelDataIn", "TunnelDataOut"}, + "Rekognition": {"SuccessfulRequestCount", "ThrottledCount", "ResponseTime", "DetectedFaceCount", "DetectedLabelCount", "ServerErrorCount", "UserErrorCount"}, "WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"}, "AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"}, "KMS": {"SecondsUntilKeyMaterialExpiration"}, @@ -145,9 +147,11 @@ func init() { "AWS/SES": {}, "AWS/SNS": {"Application", "Platform", "TopicName"}, "AWS/SQS": {"QueueName"}, + "AWS/States": {"StateMachineArn", "ActivityArn", "LambdaFunctionArn"}, "AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"}, "AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"}, "AWS/VPN": {"VpnId", "TunnelIpAddress"}, + "Rekognition": {}, "WAF": {"Rule", "WebACL"}, "AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"}, "KMS": {"KeyId"}, From ebbc079853a88787973fb8283facb524f6571a18 Mon Sep 17 00:00:00 2001 From: Sven Klemm <31455525+svenklemm@users.noreply.github.com> Date: Wed, 14 Feb 2018 15:21:00 +0100 Subject: [PATCH 30/73] improve error message for invalid/unknown datatypes (#10834) --- pkg/tsdb/postgres/postgres.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index a8c96d8119c..ca96b6c7a20 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -220,14 +220,14 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co case time.Time: timestamp = float64(columnValue.UnixNano() / 1e6) default: - return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp") + return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) } if metricIndex >= 0 { if columnValue, ok := values[metricIndex].(string); ok == true { metric = columnValue } else { - return fmt.Errorf("Column metric must be of type char,varchar or text") + return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex]) } } From fa1b92a12ba3780f6e3de49e974ab2ce6ef70cdd Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 14 Feb 2018 15:28:30 +0100 Subject: [PATCH 31/73] provisioning: uses unix epoch timestamps. (#10907) * provisioning: uses unix epoch timestamps. --- pkg/models/dashboards.go | 2 +- .../provisioning/dashboards/file_reader.go | 4 +-- .../sqlstore/dashboard_provisioning.go | 4 +-- .../sqlstore/dashboard_provisioning_test.go | 4 +-- pkg/services/sqlstore/migrations/common.go | 20 ++++++++++++++ .../sqlstore/migrations/dashboard_mig.go | 26 ++++++++++++++++++- 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 9809fdab9eb..f5c06fc3432 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -229,7 +229,7 @@ type DashboardProvisioning struct { DashboardId int64 Name string ExternalId string - Updated time.Time + Updated int64 } type SaveProvisionedDashboardCommand struct { diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index c909878999e..8244b0ae411 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -147,7 +147,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil } provisionedData, alreadyProvisioned := provisionedDashboardRefs[path] - upToDate := alreadyProvisioned && provisionedData.Updated.Unix() == resolvedFileInfo.ModTime().Unix() + upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix() dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId) if err != nil { @@ -173,7 +173,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil } fr.log.Debug("saving new dashboard", "file", path) - dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime()} + dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()} _, err = fr.dashboardRepo.SaveProvisionedDashboard(dash, dp) return provisioningMetadata, err } diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index 54068334b4b..69409c3b873 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -26,8 +26,8 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error } cmd.Result = cmd.DashboardCmd.Result - if cmd.DashboardProvisioning.Updated.IsZero() { - cmd.DashboardProvisioning.Updated = cmd.Result.Updated + if cmd.DashboardProvisioning.Updated == 0 { + cmd.DashboardProvisioning.Updated = cmd.Result.Updated.Unix() } return saveProvionedData(sess, cmd.DashboardProvisioning, cmd.Result) diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 8b2ed7ff061..b752173b67d 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -31,7 +31,7 @@ func TestDashboardProvisioningTest(t *testing.T) { DashboardProvisioning: &models.DashboardProvisioning{ Name: "default", ExternalId: "/var/grafana.json", - Updated: now, + Updated: now.Unix(), }, } @@ -48,7 +48,7 @@ func TestDashboardProvisioningTest(t *testing.T) { So(len(query.Result), ShouldEqual, 1) So(query.Result[0].DashboardId, ShouldEqual, dashId) - So(query.Result[0].Updated.Unix(), ShouldEqual, now.Unix()) + So(query.Result[0].Updated, ShouldEqual, now.Unix()) }) }) }) diff --git a/pkg/services/sqlstore/migrations/common.go b/pkg/services/sqlstore/migrations/common.go index cf0b39f1a35..bafb8292fd9 100644 --- a/pkg/services/sqlstore/migrations/common.go +++ b/pkg/services/sqlstore/migrations/common.go @@ -24,3 +24,23 @@ func addTableRenameMigration(mg *Migrator, oldName string, newName string, versi migrationId := fmt.Sprintf("Rename table %s to %s - %s", oldName, newName, versionSuffix) mg.AddMigration(migrationId, NewRenameTableMigration(oldName, newName)) } + +func addTableReplaceMigrations(mg *Migrator, from Table, to Table, migrationVersion int64, tableDataMigration map[string]string) { + fromV := version(migrationVersion - 1) + toV := version(migrationVersion) + tmpTableName := to.Name + "_tmp_qwerty" + + createTable := fmt.Sprintf("create %v %v", to.Name, toV) + copyTableData := fmt.Sprintf("copy %v %v to %v", to.Name, fromV, toV) + dropTable := fmt.Sprintf("drop %v", tmpTableName) + + addTableRenameMigration(mg, from.Name, tmpTableName, fromV) + mg.AddMigration(createTable, NewAddTableMigration(to)) + addTableIndicesMigrations(mg, toV, to) + mg.AddMigration(copyTableData, NewCopyTableDataMigration(to.Name, tmpTableName, tableDataMigration)) + mg.AddMigration(dropTable, NewDropTableMigration(tmpTableName)) +} + +func version(v int64) string { + return fmt.Sprintf("v%v", v) +} diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 1c40e241e15..df561041b7c 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -1,6 +1,8 @@ package migrations -import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +import ( + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) func addDashboardMigration(mg *Migrator) { var dashboardV1 = Table{ @@ -192,4 +194,26 @@ func addDashboardMigration(mg *Migrator) { } mg.AddMigration("create dashboard_provisioning", NewAddTableMigration(dashboardExtrasTable)) + + dashboardExtrasTableV2 := Table{ + Name: "dashboard_provisioning", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "dashboard_id", Type: DB_BigInt, Nullable: true}, + {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "external_id", Type: DB_Text, Nullable: false}, + {Name: "updated", Type: DB_Int, Default: "0", Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"dashboard_id"}}, + {Cols: []string{"dashboard_id", "name"}, Type: IndexType}, + }, + } + + addTableReplaceMigrations(mg, dashboardExtrasTable, dashboardExtrasTableV2, 2, map[string]string{ + "id": "id", + "dashboard_id": "dashboard_id", + "name": "name", + "external_id": "external_id", + }) } From 56907eef690acce6abfb52f5e005103611e0a76f Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Feb 2018 15:30:12 +0100 Subject: [PATCH 32/73] tests: makes sure we all migrations are working --- .../sqlstore/migrations/migrations_test.go | 43 ++++++++++--------- pkg/services/sqlstore/migrator/migrator.go | 4 ++ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go index 5bddf6ff605..51aea0bbdef 100644 --- a/pkg/services/sqlstore/migrations/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -14,13 +14,15 @@ import ( var indexTypes = []string{"Unknown", "INDEX", "UNIQUE INDEX"} func TestMigrations(t *testing.T) { - //log.NewLogger(0, "console", `{"level": 0}`) - testDBs := []sqlutil.TestDB{ sqlutil.TestDB_Sqlite3, } for _, testDB := range testDBs { + sql := `select count(*) as count from migration_log` + r := struct { + Count int64 + }{} Convey("Initial "+testDB.DriverName+" migration", t, func() { x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr) @@ -28,30 +30,31 @@ func TestMigrations(t *testing.T) { sqlutil.CleanDB(x) + has, err := x.SQL(sql).Get(&r) + So(err, ShouldNotBeNil) + mg := NewMigrator(x) AddMigrations(mg) err = mg.Start() So(err, ShouldBeNil) - // tables, err := x.DBMetas() - // So(err, ShouldBeNil) - // - // fmt.Printf("\nDB Schema after migration: table count: %v\n", len(tables)) - // - // for _, table := range tables { - // fmt.Printf("\nTable: %v \n", table.Name) - // for _, column := range table.Columns() { - // fmt.Printf("\t %v \n", column.String(x.Dialect())) - // } - // - // if len(table.Indexes) > 0 { - // fmt.Printf("\n\tIndexes:\n") - // for _, index := range table.Indexes { - // fmt.Printf("\t %v (%v) %v \n", index.Name, strings.Join(index.Cols, ","), indexTypes[index.Type]) - // } - // } - // } + has, err = x.SQL(sql).Get(&r) + So(err, ShouldBeNil) + So(has, ShouldBeTrue) + expectedMigrations := mg.MigrationsCount() - 2 //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this + So(r.Count, ShouldEqual, expectedMigrations) + + mg = NewMigrator(x) + AddMigrations(mg) + + err = mg.Start() + So(err, ShouldBeNil) + + has, err = x.SQL(sql).Get(&r) + So(err, ShouldBeNil) + So(has, ShouldBeTrue) + So(r.Count, ShouldEqual, expectedMigrations) }) } } diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 64831ee46b4..a8bd36ac8a3 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -35,6 +35,10 @@ func NewMigrator(engine *xorm.Engine) *Migrator { return mg } +func (mg *Migrator) MigrationsCount() int { + return len(mg.migrations) +} + func (mg *Migrator) AddMigration(id string, m Migration) { m.SetId(id) mg.migrations = append(mg.migrations, m) From 47e363ea157142fbd9b9912b29091dcb5bb085eb Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Feb 2018 15:40:25 +0100 Subject: [PATCH 33/73] removes dependencies install for plugins this features was never intended for production. --- pkg/cmd/grafana-cli/commands/install_command.go | 9 +-------- pkg/cmd/grafana-cli/models/model.go | 3 +-- pkg/plugins/models.go | 14 +------------- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index a1b249d9c81..6e13c37d3c7 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -91,14 +91,7 @@ func InstallPlugin(pluginName, version string, c CommandLine) error { } logger.Infof("%s Installed %s successfully \n", color.GreenString("✔"), pluginName) - - res, _ := s.ReadPlugin(pluginFolder, pluginName) - for _, v := range res.Dependencies.Plugins { - InstallPlugin(v.Id, version, c) - logger.Infof("Installed dependency: %v ✔\n", v.Id) - } - - return err + return nil } func SelectVersion(plugin m.Plugin, version string) (m.Version, error) { diff --git a/pkg/cmd/grafana-cli/models/model.go b/pkg/cmd/grafana-cli/models/model.go index 0700cb9a9e4..ee3e9609090 100644 --- a/pkg/cmd/grafana-cli/models/model.go +++ b/pkg/cmd/grafana-cli/models/model.go @@ -14,8 +14,7 @@ type InstalledPlugin struct { } type Dependencies struct { - GrafanaVersion string `json:"grafanaVersion"` - Plugins []Plugin `json:"plugins"` + GrafanaVersion string `json:"grafanaVersion"` } type PluginInfo struct { diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 541b37c8a8a..a3c49cf40f4 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -59,10 +59,6 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error { plog.Info("Registering plugin", "name", pb.Name) } - if len(pb.Dependencies.Plugins) == 0 { - pb.Dependencies.Plugins = []PluginDependencyItem{} - } - if pb.Dependencies.GrafanaVersion == "" { pb.Dependencies.GrafanaVersion = "*" } @@ -79,8 +75,7 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error { } type PluginDependencies struct { - GrafanaVersion string `json:"grafanaVersion"` - Plugins []PluginDependencyItem `json:"plugins"` + GrafanaVersion string `json:"grafanaVersion"` } type PluginInclude struct { @@ -96,13 +91,6 @@ type PluginInclude struct { Id string `json:"-"` } -type PluginDependencyItem struct { - Type string `json:"type"` - Id string `json:"id"` - Name string `json:"name"` - Version string `json:"version"` -} - type PluginInfo struct { Author PluginInfoLink `json:"author"` Description string `json:"description"` From 842f4c1d323afaca45dfb8e937aa62cb504820d9 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Feb 2018 16:39:05 +0100 Subject: [PATCH 34/73] tech: dont print error message on 500 page closes #10828 --- pkg/middleware/recovery.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index 388acc15afc..a8bdf809637 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -115,11 +115,11 @@ func Recovery() macaron.Handler { c.Data["Title"] = "Server Error" c.Data["AppSubUrl"] = setting.AppSubUrl - if theErr, ok := err.(error); ok { - c.Data["Title"] = theErr.Error() - } - if setting.Env == setting.DEV { + if theErr, ok := err.(error); ok { + c.Data["Title"] = theErr.Error() + } + c.Data["ErrorMsg"] = string(stack) } From ad42883fc7449a638319a433d45a59a814740afc Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Feb 2018 14:45:13 +0100 Subject: [PATCH 35/73] provisioning: adds setting to disable dashboard deletes --- docs/sources/administration/provisioning.md | 5 +- .../dashboards/config_reader_test.go | 3 + .../provisioning/dashboards/file_reader.go | 42 +++++++----- .../dashboards-from-disk/dev-dashboards.yaml | 1 + .../test-configs/version-0/version-0.yaml | 1 + pkg/services/provisioning/dashboards/types.go | 65 ++++++++++--------- 6 files changed, 68 insertions(+), 49 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 924a8245509..d213a786cd7 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -184,11 +184,14 @@ providers: orgId: 1 folder: '' type: file + disableDeletion: false + editable: false options: - folder: /var/lib/grafana/dashboards + path: /var/lib/grafana/dashboards ``` When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated. +By default Grafana will delete dashboards in the database if the file is removed. You can disable this behavior using the `disableDeletion` setting. > **Note.** Provisioning allows you to overwrite existing dashboards > which leads to problems if you re-use settings that are supposed to be unique. diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index 95f9b3561ea..ecbf6435c36 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -67,6 +67,8 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(ds.Editable, ShouldBeTrue) So(len(ds.Options), ShouldEqual, 1) So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") + So(ds.DisableDeletion, ShouldBeTrue) + ds2 := cfg[1] So(ds2.Name, ShouldEqual, "default") So(ds2.Type, ShouldEqual, "file") @@ -75,4 +77,5 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(ds2.Editable, ShouldBeFalse) So(len(ds2.Options), ShouldEqual, 1) So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") + So(ds2.DisableDeletion, ShouldBeFalse) } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index c909878999e..61dde0f454b 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -105,24 +105,7 @@ func (fr *fileReader) startWalkingDisk() error { return err } - // find dashboards to delete since json file is missing - var dashboardToDelete []int64 - for path, provisioningData := range provisionedDashboardRefs { - _, existsOnDisk := filesFoundOnDisk[path] - if !existsOnDisk { - dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId) - } - } - - // delete dashboard that are missing json file - for _, dashboardId := range dashboardToDelete { - fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId) - cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId} - err := bus.Dispatch(cmd) - if err != nil { - fr.log.Error("failed to delete dashboard", "id", cmd.Id) - } - } + fr.deleteDashboardIfFileIsMissing(provisionedDashboardRefs, filesFoundOnDisk) sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name) @@ -138,6 +121,29 @@ func (fr *fileReader) startWalkingDisk() error { return nil } +func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) { + if fr.Cfg.DisableDeletion { + return + } + + // find dashboards to delete since json file is missing + var dashboardToDelete []int64 + for path, provisioningData := range provisionedDashboardRefs { + _, existsOnDisk := filesFoundOnDisk[path] + if !existsOnDisk { + dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId) + } + } + // delete dashboard that are missing json file + for _, dashboardId := range dashboardToDelete { + fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId) + cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId} + err := bus.Dispatch(cmd) + if err != nil { + fr.log.Error("failed to delete dashboard", "id", cmd.Id) + } + } +} func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) (provisioningMetadata, error) { provisioningMetadata := provisioningMetadata{} diff --git a/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml index b55fd303a86..e9776d69010 100644 --- a/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml +++ b/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -5,6 +5,7 @@ providers: orgId: 2 folder: 'developers' editable: true + disableDeletion: true type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml index df0e6ff3044..979e762d4d4 100644 --- a/pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml +++ b/pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml @@ -2,6 +2,7 @@ org_id: 2 folder: 'developers' editable: true + disableDeletion: true type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 0fdc7b0c3ca..f742b321552 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -10,21 +10,23 @@ import ( ) type DashboardsAsConfig struct { - Name string - Type string - OrgId int64 - Folder string - Editable bool - Options map[string]interface{} + Name string + Type string + OrgId int64 + Folder string + Editable bool + Options map[string]interface{} + DisableDeletion bool } type DashboardsAsConfigV0 struct { - Name string `json:"name" yaml:"name"` - Type string `json:"type" yaml:"type"` - OrgId int64 `json:"org_id" yaml:"org_id"` - Folder string `json:"folder" yaml:"folder"` - Editable bool `json:"editable" yaml:"editable"` - Options map[string]interface{} `json:"options" yaml:"options"` + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"org_id" yaml:"org_id"` + Folder string `json:"folder" yaml:"folder"` + Editable bool `json:"editable" yaml:"editable"` + Options map[string]interface{} `json:"options" yaml:"options"` + DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` } type ConfigVersion struct { @@ -36,12 +38,13 @@ type DashboardAsConfigV1 struct { } type DashboardProviderConfigs struct { - Name string `json:"name" yaml:"name"` - Type string `json:"type" yaml:"type"` - OrgId int64 `json:"orgId" yaml:"orgId"` - Folder string `json:"folder" yaml:"folder"` - Editable bool `json:"editable" yaml:"editable"` - Options map[string]interface{} `json:"options" yaml:"options"` + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"orgId" yaml:"orgId"` + Folder string `json:"folder" yaml:"folder"` + Editable bool `json:"editable" yaml:"editable"` + Options map[string]interface{} `json:"options" yaml:"options"` + DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` } func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) { @@ -68,12 +71,13 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig for _, v := range v0 { r = append(r, &DashboardsAsConfig{ - Name: v.Name, - Type: v.Type, - OrgId: v.OrgId, - Folder: v.Folder, - Editable: v.Editable, - Options: v.Options, + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + DisableDeletion: v.DisableDeletion, }) } @@ -85,12 +89,13 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { for _, v := range dc.Providers { r = append(r, &DashboardsAsConfig{ - Name: v.Name, - Type: v.Type, - OrgId: v.OrgId, - Folder: v.Folder, - Editable: v.Editable, - Options: v.Options, + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + DisableDeletion: v.DisableDeletion, }) } From fcca578f41d6e22189d4c105f1f75bebcaa52fdb Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Thu, 15 Feb 2018 09:38:39 +0100 Subject: [PATCH 36/73] updates readmes for mysql and postgres (#10913) ref #10813 --- public/app/plugins/datasource/mysql/README.md | 14 ++++++++++++-- public/app/plugins/datasource/postgres/README.md | 10 +++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/mysql/README.md b/public/app/plugins/datasource/mysql/README.md index a8319c8dec5..4143f69be08 100644 --- a/public/app/plugins/datasource/mysql/README.md +++ b/public/app/plugins/datasource/mysql/README.md @@ -1,3 +1,13 @@ -# Grafana Fake Data Datasource - Native Plugin +# Mysql Datasource - Native Plugin -This is the built in Fake Data Datasource that is used before any datasources are set up in your Grafana installation. It means you can create a graph without any data and still get an idea of what it would look like. +Grafana ships with a built-in MySQL data source plugin that allow you to query any visualize data from a MySQL compatible database. + +##Adding the data source +1. Open the side menu by clicking the Grafana icon in the top header. +2. In the side menu under the Dashboards link you should find a link named Data Sources. +3. Click the + Add data source button in the top header. +4. Select MySQL from the Type dropdown. + +Read more about it here: + +[http://docs.grafana.org/features/datasources/mysql/](http://docs.grafana.org/features/datasources/mysql/) diff --git a/public/app/plugins/datasource/postgres/README.md b/public/app/plugins/datasource/postgres/README.md index 7b343ba78ff..16ee86a4c8c 100644 --- a/public/app/plugins/datasource/postgres/README.md +++ b/public/app/plugins/datasource/postgres/README.md @@ -1,3 +1,11 @@ # Grafana PostgreSQL Datasource - Native Plugin -This is the built in PostgreSQL Datasource that is used to connect to PostgreSQL databases. +Grafana ships with a built-in PostgreSQL data source plugin that allows you to query and visualize data from a PostgreSQL compatible database. + +##Adding the data source +1. Open the side menu by clicking the Grafana icon in the top header. +2. In the side menu under the Dashboards link you should find a link named Data Sources. +3. Click the + Add data source button in the top header. +4. Select PostgreSQL from the Type dropdown. + +[http://docs.grafana.org/features/datasources/postgres/](http://docs.grafana.org/features/datasources/postgres/) From 8954559cbd82c4dd035edd989cac67540315f57a Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Feb 2018 09:56:13 +0100 Subject: [PATCH 37/73] dashboard: whitelist allowed chars for uid --- pkg/services/dashboards/dashboards.go | 5 +++ pkg/services/dashboards/dashboards_test.go | 43 ++++++++++++++++++++++ pkg/util/shortid_generator.go | 24 +++++++++++- pkg/util/shortid_generator_test.go | 12 ++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 pkg/services/dashboards/dashboards_test.go create mode 100644 pkg/util/shortid_generator_test.go diff --git a/pkg/services/dashboards/dashboards.go b/pkg/services/dashboards/dashboards.go index b0392f7944f..9bf4eb6faec 100644 --- a/pkg/services/dashboards/dashboards.go +++ b/pkg/services/dashboards/dashboards.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/util" ) type Repository interface { @@ -52,6 +53,10 @@ func (dr *DashboardRepository) buildSaveDashboardCommand(dto *SaveDashboardDTO) return nil, models.ErrDashboardTitleEmpty } + if err := util.VerifyUid(dashboard.Uid); err != nil { + return nil, err + } + validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{ OrgId: dto.OrgId, Dashboard: dashboard, diff --git a/pkg/services/dashboards/dashboards_test.go b/pkg/services/dashboards/dashboards_test.go new file mode 100644 index 00000000000..3cf6cd1f489 --- /dev/null +++ b/pkg/services/dashboards/dashboards_test.go @@ -0,0 +1,43 @@ +package dashboards + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/util" +) + +func TestDashboardsService(t *testing.T) { + + bus.ClearBusHandlers() + + bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error { + return nil + }) + + testCases := []struct { + Uid string + Error error + }{ + {Uid: "", Error: nil}, + {Uid: "asdf90_-", Error: nil}, + {Uid: "asdf/90", Error: util.ErrDashboardInvalidUid}, + {Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: util.ErrDashboardUidToLong}, + } + + repo := &DashboardRepository{} + + for _, tc := range testCases { + dto := &SaveDashboardDTO{ + Dashboard: &models.Dashboard{Title: "title", Uid: tc.Uid}, + } + + _, err := repo.buildSaveDashboardCommand(dto) + + if err != tc.Error { + t.Fatalf("expected %s to return %v", tc.Uid, tc.Error) + } + } +} diff --git a/pkg/util/shortid_generator.go b/pkg/util/shortid_generator.go index 067f7c756ba..ca65902e869 100644 --- a/pkg/util/shortid_generator.go +++ b/pkg/util/shortid_generator.go @@ -1,11 +1,33 @@ package util import ( + "errors" + "regexp" + "github.com/teris-io/shortid" ) +var allowedChars = shortid.DefaultABC + +var validUidPattern = regexp.MustCompile(`^[a-zA-Z0-9\-\_]*$`).MatchString + +var ErrDashboardInvalidUid = errors.New("uid contains illegal characters") +var ErrDashboardUidToLong = errors.New("uid to long. max 40 characters") + +func VerifyUid(uid string) error { + if len(uid) > 40 { + return ErrDashboardUidToLong + } + + if !validUidPattern(uid) { + return ErrDashboardInvalidUid + } + + return nil +} + func init() { - gen, _ := shortid.New(1, shortid.DefaultABC, 1) + gen, _ := shortid.New(1, allowedChars, 1) shortid.SetDefault(gen) } diff --git a/pkg/util/shortid_generator_test.go b/pkg/util/shortid_generator_test.go new file mode 100644 index 00000000000..548163267dc --- /dev/null +++ b/pkg/util/shortid_generator_test.go @@ -0,0 +1,12 @@ +package util + +import "testing" + +func TestAllowedCharMatchesUidPattern(t *testing.T) { + for _, c := range allowedChars { + err := VerifyUid(string(c)) + if err != nil { + t.Fatalf("charset for creating new shortids contains chars not present in uid pattern") + } + } +} From 0ab0343995f21bb357f10f3950e6488a8a14a5ed Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Feb 2018 10:56:29 +0100 Subject: [PATCH 38/73] mark redirect_to cookie as http only closes #10829 --- pkg/middleware/auth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 826287e12f3..65697a616ea 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -51,7 +51,8 @@ func notAuthorized(c *Context) { return } - c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/") + c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/", nil, false, true) + c.Redirect(setting.AppSubUrl + "/login") } From b4c51f882255d270283e4fe35c5187accbb8c5a2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 15 Feb 2018 12:56:57 +0300 Subject: [PATCH 39/73] Fix phantomjs legend rendering issue, #10526 --- public/app/plugins/panel/graph/legend.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index cd43ac58469..2c4f7c6b01b 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -234,6 +234,24 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { elem.append(seriesElements); } + // Phantomjs has rendering issue for CSS float property, so when legend values are present, legend takes + // all graph width and rendering fails. So it should be handled to avoid rendering timeout. + // See issue #10526 https://github.com/grafana/grafana/issues/10526 + if (panel.legend.rightSide) { + const legendWidth = elem.parent().width(); + const panelWidth = elem.parent().width(); + let maxLegendElementWidth = _.max(_.map(seriesElements, el => $(el).width())); + maxLegendElementWidth = _.isNumber(maxLegendElementWidth) ? maxLegendElementWidth : 0; + const widthDiff = Math.abs(panelWidth - maxLegendElementWidth); + // Set width to content size, but table takes all space anyway, so width shouldn't be more + // than 40% of panel in this case. + if (widthDiff < panelWidth * 0.1 || legendWidth > panelWidth * 0.9) { + const maxTableWidthPercent = 0.4; + const maxWidth = Math.min(Math.ceil(maxLegendElementWidth * 1.05), panelWidth * maxTableWidthPercent); + elem.css('max-width', maxWidth); + } + } + if (!panel.legend.rightSide) { addScrollbar(); } else { From 57103ec98ad54d294170f2108f21f0bddbfb9b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Feb 2018 12:42:17 +0100 Subject: [PATCH 40/73] fix: refactoring #10922 --- public/app/core/components/grafana_app.ts | 5 ++++ public/app/plugins/panel/graph/legend.ts | 18 ------------ public/sass/components/_dashboard_grid.scss | 4 +-- public/sass/components/_panel_graph.scss | 20 ++++++++----- tools/phantomjs/render.js | 32 ++++++++++----------- 5 files changed, 36 insertions(+), 43 deletions(-) diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 70a1bda3e8b..798a40cb1bf 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -87,6 +87,11 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop elem.toggleClass('playlist-active', newValue === true); }); + // check if we are in server side render + if (document.cookie.indexOf('renderKey') !== -1) { + body.addClass('body--phantomjs'); + } + // tooltip removal fix // manage page classes var pageClass; diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 2c4f7c6b01b..cd43ac58469 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -234,24 +234,6 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { elem.append(seriesElements); } - // Phantomjs has rendering issue for CSS float property, so when legend values are present, legend takes - // all graph width and rendering fails. So it should be handled to avoid rendering timeout. - // See issue #10526 https://github.com/grafana/grafana/issues/10526 - if (panel.legend.rightSide) { - const legendWidth = elem.parent().width(); - const panelWidth = elem.parent().width(); - let maxLegendElementWidth = _.max(_.map(seriesElements, el => $(el).width())); - maxLegendElementWidth = _.isNumber(maxLegendElementWidth) ? maxLegendElementWidth : 0; - const widthDiff = Math.abs(panelWidth - maxLegendElementWidth); - // Set width to content size, but table takes all space anyway, so width shouldn't be more - // than 40% of panel in this case. - if (widthDiff < panelWidth * 0.1 || legendWidth > panelWidth * 0.9) { - const maxTableWidthPercent = 0.4; - const maxWidth = Math.min(Math.ceil(maxLegendElementWidth * 1.05), panelWidth * maxTableWidthPercent); - elem.css('max-width', maxWidth); - } - } - if (!panel.legend.rightSide) { addScrollbar(); } else { diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index 8af9a173312..0a27df75164 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -41,8 +41,8 @@ .theme-dark { .react-grid-item > .react-resizable-handle::after { - border-right: 2px solid $gray-4; - border-bottom: 2px solid $gray-4; + border-right: 2px solid $gray-1; + border-bottom: 2px solid $gray-1; } } diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index d03c8e0efb3..d60b298d3d4 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -70,19 +70,19 @@ font-size: 85%; text-align: left; &.current::before { - content: "Current: "; + content: 'Current: '; } &.max::before { - content: "Max: "; + content: 'Max: '; } &.min::before { - content: "Min: "; + content: 'Min: '; } &.total::before { - content: "Total: "; + content: 'Total: '; } &.avg::before { - content: "Avg: "; + content: 'Avg: '; } } @@ -106,6 +106,12 @@ padding-left: 6px; } +.body--phantomjs { + .graph-legend-table { + display: table; + } +} + .graph-legend-table { tbody { display: block; @@ -124,7 +130,7 @@ float: none; .graph-legend-alias::after { - content: "(right-y)"; + content: '(right-y)'; padding: 0 5px; color: $text-color-weak; } @@ -175,7 +181,7 @@ &.total, &.avg { &::before { - content: ""; + content: ''; } } } diff --git a/tools/phantomjs/render.js b/tools/phantomjs/render.js index 6ae9b5773b0..77527585589 100644 --- a/tools/phantomjs/render.js +++ b/tools/phantomjs/render.js @@ -1,42 +1,42 @@ (function() { 'use strict'; - + var page = require('webpage').create(); var args = require('system').args; var params = {}; var regexp = /^([^=]+)=([^$]+)/; - + args.forEach(function(arg) { var parts = arg.match(regexp); if (!parts) { return; } params[parts[1]] = parts[2]; }); - + var usage = "url= png= width= height= renderKey="; - + if (!params.url || !params.png || !params.renderKey || !params.domain) { console.log(usage); phantom.exit(); } - + phantom.addCookie({ 'name': 'renderKey', 'value': params.renderKey, 'domain': params.domain, }); - + page.viewportSize = { width: params.width || '800', height: params.height || '400' }; - + var timeoutMs = (parseInt(params.timeout) || 10) * 1000; var waitBetweenReadyCheckMs = 50; var totalWaitMs = 0; - + page.open(params.url, function (status) { console.log('Loading a web page: ' + params.url + ' status: ' + status, timeoutMs); - + page.onError = function(msg, trace) { var msgStack = ['ERROR: ' + msg]; if (trace && trace.length) { @@ -47,32 +47,32 @@ } console.error(msgStack.join('\n')); }; - + function checkIsReady() { var panelsRendered = page.evaluate(function() { if (!window.angular) { return false; } var body = window.angular.element(document.body); if (!body.injector) { return false; } if (!body.injector()) { return false; } - + var rootScope = body.injector().get('$rootScope'); if (!rootScope) {return false;} var panels = angular.element('div.panel:visible').length; return rootScope.panelsRendered >= panels; }); - + if (panelsRendered || totalWaitMs > timeoutMs) { var bb = page.evaluate(function () { return document.getElementsByClassName("main-view")[0].getBoundingClientRect(); }); - + page.clipRect = { top: bb.top, left: bb.left, width: bb.width, height: bb.height }; - + page.render(params.png); phantom.exit(); } else { @@ -80,7 +80,7 @@ setTimeout(checkIsReady, waitBetweenReadyCheckMs); } } - + setTimeout(checkIsReady, waitBetweenReadyCheckMs); }); - })(); \ No newline at end of file + })(); From 168ac314fca20c14a26762255d0b15ecb521a1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Feb 2018 13:20:50 +0100 Subject: [PATCH 41/73] fix: more phantomjs fixes --- public/sass/components/_panel_graph.scss | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index d60b298d3d4..716778096d6 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -16,6 +16,10 @@ padding-left: 0px; } + .graph-legend-table { + width: auto; + } + .graph-legend-table .graph-legend-series { display: table-row; } @@ -45,7 +49,7 @@ .graph-legend { flex: 0 1 auto; max-height: 30%; - margin: 0 $spacer; + margin: 0; text-align: center; padding-top: 6px; position: relative; @@ -106,9 +110,12 @@ padding-left: 6px; } +// fix for phantomjs .body--phantomjs { - .graph-legend-table { - display: table; + .graph-panel--legend-right { + .graph-legend-table { + display: table; + } } } @@ -120,6 +127,7 @@ height: 100%; padding-bottom: 1px; padding-right: 5px; + padding-left: 5px; } .graph-legend-series { From ed546c97208be444469947de41ce03e8ab0fc1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Feb 2018 13:30:40 +0100 Subject: [PATCH 42/73] updated package.json version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07014ef47d4..79f7c3f84c0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.0.0-beta1", + "version": "5.0.0-beta2", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From 4fb7ba454e6f1e1e1e2b94bb1793d833f50aec74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Feb 2018 13:41:48 +0100 Subject: [PATCH 43/73] docs: Updated changelog --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6508c4ff76a..1ebcaa779ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,16 @@ -# 5.0.0-beta2 (unrelased) +# 5.0.0-beta2 (2018-02-15) + +### Fixes + +- **Permissions** Fixed search permissions issues [#10822](https://github.com/grafana/grafana/issues/10822) +- **Permissions** Fixed problem issues displaying permissions lists [#10864](https://github.com/grafana/grafana/issues/10864) +- **PNG-Rendering** Fixed problem rendering legend to the right [#10526](https://github.com/grafana/grafana/issues/10526) +- **Reset password** Fixed problem with reset password form [#10870](https://github.com/grafana/grafana/issues/10870) +- **Light theme** Fixed problem with light theme in safari, [#10869](https://github.com/grafana/grafana/issues/10869) +- **Provisioning** Now handles deletes when dashboard json files removed from disk [#10865](https://github.com/grafana/grafana/issues/10865) +- **MySQL** Fixed issue with schema migration on old mysql (index too long) [#10779](https://github.com/grafana/grafana/issues/10779) +- **Github OAuth** Fixed fetching github orgs from private github org [#10823](https://github.com/grafana/grafana/issues/10823) +- **Embedding** Fixed issues embedding panel [#10787](https://github.com/grafana/grafana/issues/10787) # 5.0.0-beta1 (2018-02-05) From 6b930df4d42f10a0bea75ddc02edde006f21df7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Feb 2018 14:47:52 +0100 Subject: [PATCH 44/73] updated download links --- docs/sources/guides/whats-new-in-v5.md | 2 +- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 4 ++-- docs/sources/installation/windows.md | 2 +- packaging/publish/publish_testing.sh | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index bd960ed1694..fdc3c515a79 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -12,7 +12,7 @@ weight = -6 # What's New in Grafana v5.0 -> Out in beta: [Download now!](https://grafana.com/grafana/download/5.0.0-beta1) +> Out in beta: [Download now!](https://grafana.com/grafana/download/beta) This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements. diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index bfc7fdc0a3d..435d827f13d 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -16,7 +16,7 @@ weight = 1 Description | Download ------------ | ------------- Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb) -Beta for Debian-based Linux | [grafana_5.0.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta1_amd64.deb) +Beta for Debian-based Linux | [grafana_5.0.0-beta2_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta2_amd64.deb) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -33,9 +33,9 @@ sudo dpkg -i grafana_4.6.3_amd64.deb ## Install Latest Beta ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta1_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta2_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.0.0-beta1_amd64.deb +sudo dpkg -i grafana_5.0.0-beta2_amd64.deb ``` ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index f0c498c819f..dc260b84611 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -16,7 +16,7 @@ weight = 2 Description | Download ------------ | ------------- Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm) -Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.x86_64.rpm) +Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0-beta2 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta2.x86_64.rpm) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -32,7 +32,7 @@ $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/g ## Install Beta ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta2.x86_64.rpm ``` Or install manually using `rpm`. diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 08d234d63f9..b1525be5bf6 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -14,7 +14,7 @@ weight = 3 Description | Download ------------ | ------------- Latest stable package for Windows | [grafana.4.6.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3.windows-x64.zip) -Latest beta package for Windows | [grafana.5.0.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.windows-x64.zip) +Latest beta package for Windows | [grafana.5.0.0-beta2.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta2.windows-x64.zip) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. diff --git a/packaging/publish/publish_testing.sh b/packaging/publish/publish_testing.sh index ca5e7aea90c..0cbb04fd724 100755 --- a/packaging/publish/publish_testing.sh +++ b/packaging/publish/publish_testing.sh @@ -1,6 +1,6 @@ #! /usr/bin/env bash -deb_ver=5.0.0-beta1 -rpm_ver=5.0.0-beta1 +deb_ver=5.0.0-beta2 +rpm_ver=5.0.0-beta2 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb From 651103bdda26e248c427a16b49a54e8313c28150 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Feb 2018 14:56:52 +0100 Subject: [PATCH 45/73] chore: adds comment for exported function --- pkg/util/shortid_generator.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/util/shortid_generator.go b/pkg/util/shortid_generator.go index ca65902e869..f2d9faa61c8 100644 --- a/pkg/util/shortid_generator.go +++ b/pkg/util/shortid_generator.go @@ -14,6 +14,12 @@ var validUidPattern = regexp.MustCompile(`^[a-zA-Z0-9\-\_]*$`).MatchString var ErrDashboardInvalidUid = errors.New("uid contains illegal characters") var ErrDashboardUidToLong = errors.New("uid to long. max 40 characters") +func init() { + gen, _ := shortid.New(1, allowedChars, 1) + shortid.SetDefault(gen) +} + +// VerifyUid verifies the size and content of the uid func VerifyUid(uid string) error { if len(uid) > 40 { return ErrDashboardUidToLong @@ -26,11 +32,6 @@ func VerifyUid(uid string) error { return nil } -func init() { - gen, _ := shortid.New(1, allowedChars, 1) - shortid.SetDefault(gen) -} - // GenerateShortUid generates a short unique identifier. func GenerateShortUid() string { return shortid.MustGenerate() From 3ddfd8bd09bb52dda2b7f4dd30a43eb350b19c12 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 15 Feb 2018 16:47:30 +0100 Subject: [PATCH 46/73] alert notifiers: better error messages. --- .../alerting/notification_edit_ctrl.ts | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 5cada3035a4..bca6f6e8137 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -58,15 +58,29 @@ export class AlertNotificationEditCtrl { } if (this.model.id) { - this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => { - this.model = res; - appEvents.emit('alert-success', ['Notification updated', '']); - }); + this.backendSrv + .put(`/api/alert-notifications/${this.model.id}`, this.model) + .then(res => { + this.model = res; + appEvents.emit('alert-success', ['Notification updated', '']); + }) + .catch(err => { + if (err.data && err.data.error) { + appEvents.emit('alert-error', [err.data.error]); + } + }); } else { - this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => { - appEvents.emit('alert-success', ['Notification created', '']); - this.$location.path('alerting/notifications'); - }); + this.backendSrv + .post(`/api/alert-notifications`, this.model) + .then(res => { + appEvents.emit('alert-success', ['Notification created', '']); + this.$location.path('alerting/notifications'); + }) + .catch(err => { + if (err.data && err.data.error) { + appEvents.emit('alert-error', [err.data.error]); + } + }); } } From 6fa46d9539eb89fa7c70d29c6c55153985736e4f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 15 Feb 2018 15:10:16 +0100 Subject: [PATCH 47/73] plugins: update meta data for all core plugins So that the readme's can be published on Grafana.com --- public/app/plugins/datasource/cloudwatch/README.md | 4 ++-- public/app/plugins/datasource/cloudwatch/plugin.json | 4 +++- public/app/plugins/datasource/elasticsearch/README.md | 2 +- public/app/plugins/datasource/elasticsearch/plugin.json | 2 +- public/app/plugins/datasource/graphite/README.md | 6 +++++- public/app/plugins/datasource/graphite/plugin.json | 8 +++++++- public/app/plugins/datasource/influxdb/README.md | 6 ++---- public/app/plugins/datasource/influxdb/plugin.json | 4 +++- public/app/plugins/datasource/mysql/README.md | 5 +++-- public/app/plugins/datasource/mysql/plugin.json | 4 +++- public/app/plugins/datasource/opentsdb/README.md | 4 ++-- public/app/plugins/datasource/opentsdb/plugin.json | 4 +++- public/app/plugins/datasource/postgres/README.md | 5 +++-- public/app/plugins/datasource/postgres/plugin.json | 4 +++- public/app/plugins/datasource/prometheus/README.md | 2 +- public/app/plugins/datasource/prometheus/plugin.json | 7 ++++++- public/app/plugins/panel/alertlist/README.md | 8 ++++++++ public/app/plugins/panel/alertlist/plugin.json | 6 ++++-- public/app/plugins/panel/dashlist/plugin.json | 4 +++- public/app/plugins/panel/graph/plugin.json | 4 +++- public/app/plugins/panel/heatmap/README.md | 7 +++++++ public/app/plugins/panel/heatmap/plugin.json | 8 +++++++- public/app/plugins/panel/pluginlist/README.md | 1 + public/app/plugins/panel/pluginlist/plugin.json | 4 +++- public/app/plugins/panel/singlestat/plugin.json | 4 +++- public/app/plugins/panel/table/README.md | 2 +- public/app/plugins/panel/table/plugin.json | 4 +++- public/app/plugins/panel/text/plugin.json | 3 ++- 28 files changed, 93 insertions(+), 33 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/README.md b/public/app/plugins/datasource/cloudwatch/README.md index a1b7bf5cc50..5e420a4fada 100644 --- a/public/app/plugins/datasource/cloudwatch/README.md +++ b/public/app/plugins/datasource/cloudwatch/README.md @@ -1,7 +1,7 @@ -# CloudWatch Datasource - Native Plugin +# CloudWatch Data Source - Native Plugin Grafana ships with **built in** support for CloudWatch. You just have to add it as a data source and you will be ready to build dashboards for you CloudWatch metrics. Read more about it here: -[http://docs.grafana.org/datasources/cloudwatch/](http://docs.grafana.org/datasources/cloudwatch/) \ No newline at end of file +[http://docs.grafana.org/datasources/cloudwatch/](http://docs.grafana.org/datasources/cloudwatch/) diff --git a/public/app/plugins/datasource/cloudwatch/plugin.json b/public/app/plugins/datasource/cloudwatch/plugin.json index 3af7d8ccb6e..20cfcf661c5 100644 --- a/public/app/plugins/datasource/cloudwatch/plugin.json +++ b/public/app/plugins/datasource/cloudwatch/plugin.json @@ -8,6 +8,7 @@ "annotations": true, "info": { + "description": "Cloudwatch Data Source for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -15,6 +16,7 @@ "logos": { "small": "img/amazon-web-services.png", "large": "img/amazon-web-services.png" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/datasource/elasticsearch/README.md b/public/app/plugins/datasource/elasticsearch/README.md index 22445b022fe..fd1f2f74f7a 100644 --- a/public/app/plugins/datasource/elasticsearch/README.md +++ b/public/app/plugins/datasource/elasticsearch/README.md @@ -1,4 +1,4 @@ -# Elasticsearch Datasource - Native Plugin +# Elasticsearch Data Source - Native Plugin Grafana ships with **advanced support** for Elasticsearch. You can do many types of simple or complex elasticsearch queries to visualize logs or metrics stored in Elasticsearch. You can also annotate your graphs with log events stored in Elasticsearch. diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index f86c2f8f89f..59d26b785ac 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -17,7 +17,7 @@ "links": [ {"name": "elastic.co", "url": "https://www.elastic.co/products/elasticsearch"} ], - "version": "3.0.0" + "version": "5.0.0" }, "annotations": true, diff --git a/public/app/plugins/datasource/graphite/README.md b/public/app/plugins/datasource/graphite/README.md index c27c5789bca..68e3a36da63 100644 --- a/public/app/plugins/datasource/graphite/README.md +++ b/public/app/plugins/datasource/graphite/README.md @@ -6,4 +6,8 @@ Grafana has an advanced Graphite query editor that lets you quickly navigate the Read more about it here: -[http://docs.grafana.org/datasources/graphite/](http://docs.grafana.org/datasources/graphite/) \ No newline at end of file +[http://docs.grafana.org/datasources/graphite/](http://docs.grafana.org/datasources/graphite/) + +Graphite 1.1 Release: + +[https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/](https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/) diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index 366c56f62c8..1448242ed05 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -17,6 +17,7 @@ }, "info": { + "description": "Graphite Data Source for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -24,6 +25,11 @@ "logos": { "small": "img/graphite_logo.png", "large": "img/graphite_logo.png" - } + }, + "links": [ + {"name": "Graphite", "url": "https://graphiteapp.org/"}, + {"name": "Graphite 1.1 Release", "url": "https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/"} + ], + "version": "5.0.0" } } diff --git a/public/app/plugins/datasource/influxdb/README.md b/public/app/plugins/datasource/influxdb/README.md index aca126db36c..bc75de4feb6 100644 --- a/public/app/plugins/datasource/influxdb/README.md +++ b/public/app/plugins/datasource/influxdb/README.md @@ -1,10 +1,8 @@ # InfluxDB Datasource - Native Plugin -Grafana ships with **built in** support for InfluxDB 0.9. +Grafana ships with **built in** support for InfluxDB (> 0.9.x). -There are currently two separate datasources for InfluxDB in Grafana: InfluxDB 0.8.x and InfluxDB 0.9.x. The API and capabilities of InfluxDB 0.9.x are completely different from InfluxDB 0.8.x which is why Grafana handles them as different data sources. - -This is the plugin for InfluxDB 0.9. It is rapidly evolving and we continue to track its API. +There are currently two separate datasources for InfluxDB in Grafana: InfluxDB 0.8.x and the latest InfluxDB release. The API and capabilities of latest (> 0.9.x) InfluxDB are completely different from InfluxDB 0.8.x which is why Grafana handles them as different data sources. InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users. You can find it [here](https://grafana.com/plugins/grafana-influxdb-08-datasource). diff --git a/public/app/plugins/datasource/influxdb/plugin.json b/public/app/plugins/datasource/influxdb/plugin.json index 7300940cbb8..973c4ac52cd 100644 --- a/public/app/plugins/datasource/influxdb/plugin.json +++ b/public/app/plugins/datasource/influxdb/plugin.json @@ -13,6 +13,7 @@ }, "info": { + "description": "InfluxDB Data Source for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -20,6 +21,7 @@ "logos": { "small": "img/influxdb_logo.svg", "large": "img/influxdb_logo.svg" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/datasource/mysql/README.md b/public/app/plugins/datasource/mysql/README.md index 4143f69be08..1cdf72f462b 100644 --- a/public/app/plugins/datasource/mysql/README.md +++ b/public/app/plugins/datasource/mysql/README.md @@ -1,8 +1,9 @@ -# Mysql Datasource - Native Plugin +# MySQL Data Source - Native Plugin Grafana ships with a built-in MySQL data source plugin that allow you to query any visualize data from a MySQL compatible database. -##Adding the data source +## Adding the data source + 1. Open the side menu by clicking the Grafana icon in the top header. 2. In the side menu under the Dashboards link you should find a link named Data Sources. 3. Click the + Add data source button in the top header. diff --git a/public/app/plugins/datasource/mysql/plugin.json b/public/app/plugins/datasource/mysql/plugin.json index 5f35b3f709f..363b9364016 100644 --- a/public/app/plugins/datasource/mysql/plugin.json +++ b/public/app/plugins/datasource/mysql/plugin.json @@ -4,6 +4,7 @@ "id": "mysql", "info": { + "description": "MySQL Data Source for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -11,7 +12,8 @@ "logos": { "small": "img/mysql_logo.svg", "large": "img/mysql_logo.svg" - } + }, + "version": "5.0.0" }, "alerting": true, diff --git a/public/app/plugins/datasource/opentsdb/README.md b/public/app/plugins/datasource/opentsdb/README.md index 697ecd5c4dc..4afd5dce1b2 100644 --- a/public/app/plugins/datasource/opentsdb/README.md +++ b/public/app/plugins/datasource/opentsdb/README.md @@ -1,7 +1,7 @@ -# OpenTSDB Datasource - Native Plugin +# OpenTSDB Data Source - Native Plugin Grafana ships with **built in** support for OpenTSDB, a scalable, distributed time series database. Read more about it here: -[http://docs.grafana.org/datasources/opentsdb/](http://docs.grafana.org/datasources/opentsdb/) \ No newline at end of file +[http://docs.grafana.org/datasources/opentsdb/](http://docs.grafana.org/datasources/opentsdb/) diff --git a/public/app/plugins/datasource/opentsdb/plugin.json b/public/app/plugins/datasource/opentsdb/plugin.json index b2f4503b5ff..571c311cbcb 100644 --- a/public/app/plugins/datasource/opentsdb/plugin.json +++ b/public/app/plugins/datasource/opentsdb/plugin.json @@ -9,6 +9,7 @@ "alerting": true, "info": { + "description": "OpenTSDB Data Source for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -16,6 +17,7 @@ "logos": { "small": "img/opentsdb_logo.png", "large": "img/opentsdb_logo.png" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/datasource/postgres/README.md b/public/app/plugins/datasource/postgres/README.md index 16ee86a4c8c..75c36158418 100644 --- a/public/app/plugins/datasource/postgres/README.md +++ b/public/app/plugins/datasource/postgres/README.md @@ -1,8 +1,9 @@ -# Grafana PostgreSQL Datasource - Native Plugin +# Grafana PostgreSQL Data Source - Native Plugin Grafana ships with a built-in PostgreSQL data source plugin that allows you to query and visualize data from a PostgreSQL compatible database. -##Adding the data source +## Adding the data source + 1. Open the side menu by clicking the Grafana icon in the top header. 2. In the side menu under the Dashboards link you should find a link named Data Sources. 3. Click the + Add data source button in the top header. diff --git a/public/app/plugins/datasource/postgres/plugin.json b/public/app/plugins/datasource/postgres/plugin.json index 26d050ba8ed..af2dbc4468e 100644 --- a/public/app/plugins/datasource/postgres/plugin.json +++ b/public/app/plugins/datasource/postgres/plugin.json @@ -4,6 +4,7 @@ "id": "postgres", "info": { + "description": "PostgreSQL Data Source for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -11,7 +12,8 @@ "logos": { "small": "img/postgresql_logo.svg", "large": "img/postgresql_logo.svg" - } + }, + "version": "5.0.0" }, "alerting": true, diff --git a/public/app/plugins/datasource/prometheus/README.md b/public/app/plugins/datasource/prometheus/README.md index 5b188c3393c..2c44605c04c 100644 --- a/public/app/plugins/datasource/prometheus/README.md +++ b/public/app/plugins/datasource/prometheus/README.md @@ -1,4 +1,4 @@ -# Prometheus Datasource - Native Plugin +# Prometheus Data Source - Native Plugin Grafana ships with **built in** support for Prometheus, the open-source service monitoring system and time series database. diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index aa48a077b50..88847765159 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -18,6 +18,7 @@ }, "info": { + "description": "Prometheus Data Source for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -25,6 +26,10 @@ "logos": { "small": "img/prometheus_logo.svg", "large": "img/prometheus_logo.svg" - } + }, + "links": [ + {"name": "Prometheus", "url": "https://prometheus.io/"} + ], + "version": "5.0.0" } } diff --git a/public/app/plugins/panel/alertlist/README.md b/public/app/plugins/panel/alertlist/README.md index bc01e2ff1d1..36c305f3182 100644 --- a/public/app/plugins/panel/alertlist/README.md +++ b/public/app/plugins/panel/alertlist/README.md @@ -1 +1,9 @@ # Alert List Panel - Native plugin + +This Alert List panel is **included** with Grafana. + +The Alert List panel allows you to display alerts on a dashboard. The list can be configured to show either the current state of your alerts or recent alert state changes. You can read more about alerts [here](http://docs.grafana.org/alerting/rules). + +Read more about it here: + +[http://docs.grafana.org/features/panels/alertlist/](http://docs.grafana.org/features/panels/alertlist/) diff --git a/public/app/plugins/panel/alertlist/plugin.json b/public/app/plugins/panel/alertlist/plugin.json index 9fd42bc5ed2..ff36a572f2b 100644 --- a/public/app/plugins/panel/alertlist/plugin.json +++ b/public/app/plugins/panel/alertlist/plugin.json @@ -4,13 +4,15 @@ "id": "alertlist", "info": { + "description": "Shows list of alerts and their current status", "author": { "name": "Grafana Project", "url": "https://grafana.com" -}, + }, "logos": { "small": "img/icn-singlestat-panel.svg", "large": "img/icn-singlestat-panel.svg" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/panel/dashlist/plugin.json b/public/app/plugins/panel/dashlist/plugin.json index 3b8eca5e49d..9dcde08a598 100644 --- a/public/app/plugins/panel/dashlist/plugin.json +++ b/public/app/plugins/panel/dashlist/plugin.json @@ -4,6 +4,7 @@ "id": "dashlist", "info": { + "description": "List of dynamic links to other dashboards", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -11,6 +12,7 @@ "logos": { "small": "img/icn-dashlist-panel.svg", "large": "img/icn-dashlist-panel.svg" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/panel/graph/plugin.json b/public/app/plugins/panel/graph/plugin.json index 102c7d14e56..c0c8e8db290 100644 --- a/public/app/plugins/panel/graph/plugin.json +++ b/public/app/plugins/panel/graph/plugin.json @@ -4,6 +4,7 @@ "id": "graph", "info": { + "description": "Graph Panel for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -11,7 +12,8 @@ "logos": { "small": "img/icn-graph-panel.svg", "large": "img/icn-graph-panel.svg" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/panel/heatmap/README.md b/public/app/plugins/panel/heatmap/README.md index e69de29bb2d..1b95a02c4cb 100644 --- a/public/app/plugins/panel/heatmap/README.md +++ b/public/app/plugins/panel/heatmap/README.md @@ -0,0 +1,7 @@ +# Heatmap Panel - Native Plugin + +The Heatmap panel allows you to view histograms over time and is **included** with Grafana. + +Read more about it here: + +[http://docs.grafana.org/features/panels/heatmap/](http://docs.grafana.org/features/panels/heatmap/) diff --git a/public/app/plugins/panel/heatmap/plugin.json b/public/app/plugins/panel/heatmap/plugin.json index bee509467a7..723d8e886a0 100644 --- a/public/app/plugins/panel/heatmap/plugin.json +++ b/public/app/plugins/panel/heatmap/plugin.json @@ -4,6 +4,7 @@ "id": "heatmap", "info": { + "description": "Heatmap Panel for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -11,6 +12,11 @@ "logos": { "small": "img/icn-heatmap-panel.svg", "large": "img/icn-heatmap-panel.svg" - } + }, + "links": [ + {"name": "Brendan Gregg - Heatmaps", "url": "http://www.brendangregg.com/heatmaps.html"}, + {"name": "Brendan Gregg - Latency Heatmaps", "url": " http://www.brendangregg.com/HeatMaps/latency.html"} + ], + "version": "5.0.0" } } diff --git a/public/app/plugins/panel/pluginlist/README.md b/public/app/plugins/panel/pluginlist/README.md index 463769dad1f..3aacada938b 100644 --- a/public/app/plugins/panel/pluginlist/README.md +++ b/public/app/plugins/panel/pluginlist/README.md @@ -1,2 +1,3 @@ # Plugin List Panel - Native Plugin +The Plugin List plans shows the installed plugins for your Grafana instance and is **included** with Grafana. It is used on the default Home dashboard. diff --git a/public/app/plugins/panel/pluginlist/plugin.json b/public/app/plugins/panel/pluginlist/plugin.json index 0cdc506167c..b955177f1bc 100644 --- a/public/app/plugins/panel/pluginlist/plugin.json +++ b/public/app/plugins/panel/pluginlist/plugin.json @@ -4,6 +4,7 @@ "id": "pluginlist", "info": { + "description": "Plugin List for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -11,6 +12,7 @@ "logos": { "small": "img/icn-dashlist-panel.svg", "large": "img/icn-dashlist-panel.svg" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/panel/singlestat/plugin.json b/public/app/plugins/panel/singlestat/plugin.json index 8bf45d3b690..5e36ab5cf4e 100644 --- a/public/app/plugins/panel/singlestat/plugin.json +++ b/public/app/plugins/panel/singlestat/plugin.json @@ -4,6 +4,7 @@ "id": "singlestat", "info": { + "description": "Singlestat Panel for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -11,7 +12,8 @@ "logos": { "small": "img/icn-singlestat-panel.svg", "large": "img/icn-singlestat-panel.svg" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/panel/table/README.md b/public/app/plugins/panel/table/README.md index 48c4fac641b..98f2c13f75c 100644 --- a/public/app/plugins/panel/table/README.md +++ b/public/app/plugins/panel/table/README.md @@ -6,4 +6,4 @@ The table panel is very flexible, supporting both multiple modes for time series Check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase) or read more about it here: -[http://docs.grafana.org/reference/table_panel/](http://docs.grafana.org/reference/table_panel/) \ No newline at end of file +[http://docs.grafana.org/reference/table_panel/](http://docs.grafana.org/reference/table_panel/) diff --git a/public/app/plugins/panel/table/plugin.json b/public/app/plugins/panel/table/plugin.json index c1d2c43b128..3b6cbaae876 100644 --- a/public/app/plugins/panel/table/plugin.json +++ b/public/app/plugins/panel/table/plugin.json @@ -4,6 +4,7 @@ "id": "table", "info": { + "description": "Table Panel for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -11,7 +12,8 @@ "logos": { "small": "img/icn-table-panel.svg", "large": "img/icn-table-panel.svg" - } + }, + "version": "5.0.0" } } diff --git a/public/app/plugins/panel/text/plugin.json b/public/app/plugins/panel/text/plugin.json index 9bc604a87bb..6152bd322b5 100644 --- a/public/app/plugins/panel/text/plugin.json +++ b/public/app/plugins/panel/text/plugin.json @@ -11,7 +11,8 @@ "logos": { "small": "img/icn-text-panel.svg", "large": "img/icn-text-panel.svg" - } + }, + "version": "5.0.0" } } From b8b6dc6d6d2fb1006a510ea4de3f804eda3abc44 Mon Sep 17 00:00:00 2001 From: Scott Brenner Date: Thu, 15 Feb 2018 10:37:23 -0800 Subject: [PATCH 48/73] Minor typo fix --- conf/defaults.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 3766c829323..86768738171 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -327,7 +327,7 @@ allow_sign_up = true enabled = false host = localhost:25 user = -# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" +# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" password = cert_file = key_file = From 43baf20dcd3315147fa0ab40d0e59e6f1c1628c9 Mon Sep 17 00:00:00 2001 From: Scott Brenner Date: Thu, 15 Feb 2018 10:41:04 -0800 Subject: [PATCH 49/73] Update ldap.md --- docs/sources/installation/ldap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 8f6be6e1d8c..85501e51d85 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -43,7 +43,7 @@ ssl_skip_verify = false # Search user bind dn bind_dn = "cn=admin,dc=grafana,dc=org" # Search user bind password -# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" +# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" bind_password = 'grafana' # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" From 7535cefed9be855164aca58afd1e39c0f9266079 Mon Sep 17 00:00:00 2001 From: Scott Brenner Date: Thu, 15 Feb 2018 10:41:15 -0800 Subject: [PATCH 50/73] Update ldap.toml --- conf/ldap.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/ldap.toml b/conf/ldap.toml index ae217106cb2..166d85eabb1 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -19,7 +19,7 @@ ssl_skip_verify = false # Search user bind dn bind_dn = "cn=admin,dc=grafana,dc=org" # Search user bind password -# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" +# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" bind_password = 'grafana' # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" From 2d03ab1770608af18431a38484c384bef7abc3be Mon Sep 17 00:00:00 2001 From: Scott Brenner Date: Thu, 15 Feb 2018 10:41:26 -0800 Subject: [PATCH 51/73] Update sample.ini --- conf/sample.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/sample.ini b/conf/sample.ini index 784f6b7cfc9..5f13dad4061 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -71,7 +71,7 @@ ;host = 127.0.0.1:3306 ;name = grafana ;user = root -# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" +# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" ;password = # Use either URL or the previous fields to configure the database From 2c5f3fbc5b1cec65fca9feb3a5f9072380f55585 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 16 Feb 2018 11:11:26 +0300 Subject: [PATCH 52/73] repeat row: fix panels placement bug (#10932) --- public/app/features/dashboard/dashboard_model.ts | 3 ++- .../app/features/dashboard/specs/repeat.jest.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 055b59065ae..e35f8f0d430 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -500,11 +500,12 @@ export class DashboardModel { if (!rowPanel.panels || rowPanel.panels.length === 0) { return 0; } + const rowYPos = rowPanel.gridPos.y; const positions = _.map(rowPanel.panels, 'gridPos'); const maxPos = _.maxBy(positions, pos => { return pos.y + pos.h; }); - return maxPos.h + 1; + return maxPos.y + maxPos.h - rowYPos; } removePanel(panel: PanelModel) { diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 868db3b7246..09bb3b7c494 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -500,6 +500,22 @@ describe('given dashboard with row repeat', function() { ); expect(panel_ids.length).toEqual(_.uniq(panel_ids).length); }); + + it('should place new panels in proper order', function() { + dashboardJSON.panels = [ + { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 }, repeat: 'apps' }, + { id: 2, type: 'graph', gridPos: { x: 0, y: 1, h: 3, w: 12 } }, + { id: 3, type: 'graph', gridPos: { x: 6, y: 1, h: 4, w: 12 } }, + { id: 4, type: 'graph', gridPos: { x: 0, y: 5, h: 2, w: 12 } }, + ]; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual(['row', 'graph', 'graph', 'graph', 'row', 'graph', 'graph', 'graph']); + const panel_y_positions = _.map(dashboard.panels, p => p.gridPos.y); + expect(panel_y_positions).toEqual([0, 1, 1, 5, 7, 8, 8, 12]); + }); }); describe('given dashboard with row and panel repeat', () => { From 244ae555d9469d063ed52f7ef134ad522479a9bc Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 16 Feb 2018 09:14:32 +0100 Subject: [PATCH 53/73] Close modal with esc (#10929) * added var to check if modal is open and an if for escape fullview * moved showconfirmmodal to utils, showconfirmmodal now uses showmodal, esc works in textinput * made esc global --- package.json | 1 + public/app/core/services/alert_srv.ts | 42 +----------- public/app/core/services/keybindingSrv.ts | 56 +++++++++++----- public/app/core/services/util_srv.ts | 33 +++++++++ yarn.lock | 82 ++--------------------- 5 files changed, 80 insertions(+), 134 deletions(-) diff --git a/package.json b/package.json index 79f7c3f84c0..8cd0468b9c8 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "mobx-state-tree": "^1.3.1", "moment": "^2.18.1", "mousetrap": "^1.6.0", + "mousetrap-global-bind": "^1.1.0", "perfect-scrollbar": "^1.2.0", "prop-types": "^15.6.0", "react": "^16.2.0", diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 35e8e39804f..fc76ef9e371 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -7,7 +7,7 @@ export class AlertSrv { list: any[]; /** @ngInject */ - constructor(private $timeout, private $rootScope, private $modal) { + constructor(private $timeout, private $rootScope) { this.list = []; } @@ -39,7 +39,6 @@ export class AlertSrv { appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000)); appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000)); appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000)); - appEvents.on('confirm-modal', this.showConfirmModal.bind(this)); } getIconForSeverity(severity) { @@ -96,45 +95,6 @@ export class AlertSrv { clearAll() { this.list = []; } - - showConfirmModal(payload) { - var scope = this.$rootScope.$new(); - - scope.onConfirm = function() { - payload.onConfirm(); - scope.dismiss(); - }; - - scope.updateConfirmText = function(value) { - scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase(); - }; - - scope.title = payload.title; - scope.text = payload.text; - scope.text2 = payload.text2; - scope.confirmText = payload.confirmText; - - scope.onConfirm = payload.onConfirm; - scope.onAltAction = payload.onAltAction; - scope.altActionText = payload.altActionText; - scope.icon = payload.icon || 'fa-check'; - scope.yesText = payload.yesText || 'Yes'; - scope.noText = payload.noText || 'Cancel'; - scope.confirmTextValid = scope.confirmText ? false : true; - - var confirmModal = this.$modal({ - template: 'public/app/partials/confirm_modal.html', - persist: false, - modalClass: 'confirm-modal', - show: false, - scope: scope, - keyboard: false, - }); - - confirmModal.then(function(modalEl) { - modalEl.modal('show'); - }); - } } coreModule.service('alertSrv', AlertSrv); diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 36fe73b62ce..ce3657a4299 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -5,9 +5,11 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import Mousetrap from 'mousetrap'; +import 'mousetrap-global-bind'; export class KeybindingSrv { helpModal: boolean; + modalOpen = false; /** @ngInject */ constructor(private $rootScope, private $location) { @@ -19,6 +21,7 @@ export class KeybindingSrv { }); this.setupGlobal(); + appEvents.on('show-modal', () => (this.modalOpen = true)); } setupGlobal() { @@ -30,6 +33,7 @@ export class KeybindingSrv { this.bind('s o', this.openSearch); this.bind('s t', this.openSearchTags); this.bind('f', this.openSearch); + this.bindGlobal('esc', this.exit); } openSearchStarred() { @@ -60,6 +64,28 @@ export class KeybindingSrv { appEvents.emit('show-modal', { templateHtml: '' }); } + exit() { + var popups = $('.popover.in'); + if (popups.length > 0) { + return; + } + + appEvents.emit('hide-modal'); + + if (!this.modalOpen) { + appEvents.emit('panel-change-view', { fullscreen: false, edit: false }); + } else { + this.modalOpen = false; + } + + // close settings view + var search = this.$location.search(); + if (search.editview) { + delete search.editview; + this.$location.search(search); + } + } + bind(keyArg, fn) { Mousetrap.bind( keyArg, @@ -73,6 +99,19 @@ export class KeybindingSrv { ); } + bindGlobal(keyArg, fn) { + Mousetrap.bindGlobal( + keyArg, + evt => { + evt.preventDefault(); + evt.stopPropagation(); + evt.returnValue = false; + return this.$rootScope.$apply(fn.bind(this)); + }, + 'keydown' + ); + } + showDashEditView() { var search = _.extend(this.$location.search(), { editview: 'settings' }); this.$location.search(search); @@ -204,23 +243,6 @@ export class KeybindingSrv { this.bind('d v', () => { appEvents.emit('toggle-view-mode'); }); - - this.bind('esc', () => { - var popups = $('.popover.in'); - if (popups.length > 0) { - return; - } - - scope.appEvent('hide-modal'); - scope.appEvent('panel-change-view', { fullscreen: false, edit: false }); - - // close settings view - var search = this.$location.search(); - if (search.editview) { - delete search.editview; - this.$location.search(search); - } - }); } } diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts index 52c934ee9ee..1afae0a02f4 100644 --- a/public/app/core/services/util_srv.ts +++ b/public/app/core/services/util_srv.ts @@ -10,6 +10,7 @@ export class UtilSrv { init() { appEvents.on('show-modal', this.showModal.bind(this), this.$rootScope); appEvents.on('hide-modal', this.hideModal.bind(this), this.$rootScope); + appEvents.on('confirm-modal', this.showConfirmModal.bind(this), this.$rootScope); } hideModal() { @@ -47,6 +48,38 @@ export class UtilSrv { modalEl.modal('show'); }); } + + showConfirmModal(payload) { + var scope = this.$rootScope.$new(); + + scope.onConfirm = function() { + payload.onConfirm(); + scope.dismiss(); + }; + + scope.updateConfirmText = function(value) { + scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase(); + }; + + scope.title = payload.title; + scope.text = payload.text; + scope.text2 = payload.text2; + scope.confirmText = payload.confirmText; + + scope.onConfirm = payload.onConfirm; + scope.onAltAction = payload.onAltAction; + scope.altActionText = payload.altActionText; + scope.icon = payload.icon || 'fa-check'; + scope.yesText = payload.yesText || 'Yes'; + scope.noText = payload.noText || 'Cancel'; + scope.confirmTextValid = scope.confirmText ? false : true; + + appEvents.emit('show-modal', { + src: 'public/app/partials/confirm_modal.html', + scope: scope, + modalClass: 'confirm-modal', + }); + } } coreModule.service('utilSrv', UtilSrv); diff --git a/yarn.lock b/yarn.lock index 83a906c919d..a78fcf0c3d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -224,14 +224,6 @@ version "16.0.25" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.25.tgz#bf696b83fe480c5e0eff4335ee39ebc95884a1ed" -"@types/strip-bom@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" - -"@types/strip-json-comments@0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" - JSONStream@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -1641,14 +1633,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" -chalk@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" - dependencies: - ansi-styles "^3.2.0" - escape-string-regexp "^1.0.5" - supports-color "^5.2.0" - chalk@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" @@ -2799,7 +2783,7 @@ diff@^2.0.2: version "2.2.3" resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" -diff@^3.1.0, diff@^3.2.0: +diff@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" @@ -4414,10 +4398,6 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -4546,12 +4526,6 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" -homedir-polyfill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" - dependencies: - parse-passwd "^1.0.0" - hooker@^0.2.3, hooker@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" @@ -6250,10 +6224,6 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" -make-error@^1.1.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.3.tgz#a97ae14ffd98b05f543e83ddc395e1b2b6e4cc6a" - make-fetch-happen@^2.4.13, make-fetch-happen@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38" @@ -6555,6 +6525,10 @@ moment@^2.18.1: version "2.19.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" +mousetrap-global-bind@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz#cd7de9222bd0646fa2e010d54c84a74c26a88edd" + mousetrap@^1.6.0: version "1.6.1" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9" @@ -7414,10 +7388,6 @@ parse-json@^3.0.0: dependencies: error-ex "^1.3.1" -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - parse5@^3.0.1, parse5@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" @@ -9648,7 +9618,7 @@ strip-json-comments@1.0.x, strip-json-comments@~1.0.1, strip-json-comments@~1.0. version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" -strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: +strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -9680,12 +9650,6 @@ supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: dependencies: has-flag "^2.0.0" -supports-color@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" - dependencies: - has-flag "^3.0.0" - svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -9994,30 +9958,6 @@ ts-loader@^3.2.0: loader-utils "^1.0.2" semver "^5.0.1" -ts-node@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-4.1.0.tgz#36d9529c7b90bb993306c408cd07f7743de20712" - dependencies: - arrify "^1.0.0" - chalk "^2.3.0" - diff "^3.1.0" - make-error "^1.1.1" - minimist "^1.2.0" - mkdirp "^0.5.1" - source-map-support "^0.5.0" - tsconfig "^7.0.0" - v8flags "^3.0.0" - yn "^2.0.0" - -tsconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" - dependencies: - "@types/strip-bom" "^3.0.0" - "@types/strip-json-comments" "0.0.30" - strip-bom "^3.0.0" - strip-json-comments "^2.0.0" - tslib@^1.7.1: version "1.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" @@ -10379,12 +10319,6 @@ uuid@^3.0.0, uuid@^3.1.0, uuid@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -v8flags@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b" - dependencies: - homedir-polyfill "^1.0.1" - validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" @@ -10849,10 +10783,6 @@ yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" -yn@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" - zip-stream@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04" From 3b04efa4c081ccc3f99c686d63f3bc2d9fb42cff Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 16 Feb 2018 17:29:10 +0900 Subject: [PATCH 54/73] migrate minSpan (#10924) --- public/app/features/dashboard/dashboard_migration.ts | 3 +++ .../features/dashboard/specs/dashboard_migration.jest.ts | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index 0a6c5d141c2..87039fccdc4 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -429,6 +429,9 @@ export class DashboardMigrator { for (let panel of row.panels) { panel.span = panel.span || DEFAULT_PANEL_SPAN; + if (panel.minSpan) { + panel.minSpan = Math.min(GRID_COLUMN_COUNT, GRID_COLUMN_COUNT / 12 * panel.minSpan); + } const panelWidth = Math.floor(panel.span) * widthFactor; const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight; diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.jest.ts index 4cfbc8eecd5..4858063ac09 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.jest.ts @@ -363,6 +363,14 @@ describe('DashboardModel', function() { expect(dashboard.panels[0].repeat).toBe('server'); expect(dashboard.panels.length).toBe(2); }); + + it('minSpan should be twice', function() { + model.rows = [createRow({ height: 8 }, [[6]])]; + model.rows[0].panels[0] = { minSpan: 12 }; + + let dashboard = new DashboardModel(model); + expect(dashboard.panels[0].minSpan).toBe(24); + }); }); }); From 5bbe047eaa0c3ae054b33cd5373fa025505b2172 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 16 Feb 2018 09:49:29 +0100 Subject: [PATCH 55/73] Revert "removes dependencies install for plugins" This reverts commit 47e363ea157142fbd9b9912b29091dcb5bb085eb. --- pkg/cmd/grafana-cli/commands/install_command.go | 9 ++++++++- pkg/cmd/grafana-cli/models/model.go | 3 ++- pkg/plugins/models.go | 14 +++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 6e13c37d3c7..a1b249d9c81 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -91,7 +91,14 @@ func InstallPlugin(pluginName, version string, c CommandLine) error { } logger.Infof("%s Installed %s successfully \n", color.GreenString("✔"), pluginName) - return nil + + res, _ := s.ReadPlugin(pluginFolder, pluginName) + for _, v := range res.Dependencies.Plugins { + InstallPlugin(v.Id, version, c) + logger.Infof("Installed dependency: %v ✔\n", v.Id) + } + + return err } func SelectVersion(plugin m.Plugin, version string) (m.Version, error) { diff --git a/pkg/cmd/grafana-cli/models/model.go b/pkg/cmd/grafana-cli/models/model.go index ee3e9609090..0700cb9a9e4 100644 --- a/pkg/cmd/grafana-cli/models/model.go +++ b/pkg/cmd/grafana-cli/models/model.go @@ -14,7 +14,8 @@ type InstalledPlugin struct { } type Dependencies struct { - GrafanaVersion string `json:"grafanaVersion"` + GrafanaVersion string `json:"grafanaVersion"` + Plugins []Plugin `json:"plugins"` } type PluginInfo struct { diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index a3c49cf40f4..541b37c8a8a 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -59,6 +59,10 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error { plog.Info("Registering plugin", "name", pb.Name) } + if len(pb.Dependencies.Plugins) == 0 { + pb.Dependencies.Plugins = []PluginDependencyItem{} + } + if pb.Dependencies.GrafanaVersion == "" { pb.Dependencies.GrafanaVersion = "*" } @@ -75,7 +79,8 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error { } type PluginDependencies struct { - GrafanaVersion string `json:"grafanaVersion"` + GrafanaVersion string `json:"grafanaVersion"` + Plugins []PluginDependencyItem `json:"plugins"` } type PluginInclude struct { @@ -91,6 +96,13 @@ type PluginInclude struct { Id string `json:"-"` } +type PluginDependencyItem struct { + Type string `json:"type"` + Id string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` +} + type PluginInfo struct { Author PluginInfoLink `json:"author"` Description string `json:"description"` From b099f0309f4a0881a8007946dbaeb0c565e06caa Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 16 Feb 2018 09:54:37 +0100 Subject: [PATCH 56/73] cli: download latest dependency by default --- pkg/cmd/grafana-cli/commands/install_command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index a1b249d9c81..f40bc9c081b 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -94,7 +94,7 @@ func InstallPlugin(pluginName, version string, c CommandLine) error { res, _ := s.ReadPlugin(pluginFolder, pluginName) for _, v := range res.Dependencies.Plugins { - InstallPlugin(v.Id, version, c) + InstallPlugin(v.Id, "", c) logger.Infof("Installed dependency: %v ✔\n", v.Id) } From 4c073e1cf03d99bb57eb9ed4b7c65bfe1fb04ff9 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 16 Feb 2018 10:18:40 +0100 Subject: [PATCH 57/73] docker: add test dashboards for mysql and postgres for visualizing data generated by fake-data-gen --- docker/blocks/mysql/dashboard.json | 549 ++++++++++++++++++++++++++ docker/blocks/postgres/dashboard.json | 547 +++++++++++++++++++++++++ 2 files changed, 1096 insertions(+) create mode 100644 docker/blocks/mysql/dashboard.json create mode 100644 docker/blocks/postgres/dashboard.json diff --git a/docker/blocks/mysql/dashboard.json b/docker/blocks/mysql/dashboard.json new file mode 100644 index 00000000000..e2b791f82e6 --- /dev/null +++ b/docker/blocks/mysql/dashboard.json @@ -0,0 +1,549 @@ +{ + "__inputs": [ + { + "name": "DS_MYSQL", + "label": "Mysql", + "description": "", + "type": "datasource", + "pluginId": "mysql", + "pluginName": "MySQL" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "mysql", + "name": "MySQL", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "A dashboard visualizing data generated from grafana/fake-data-gen", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1518602729468, + "links": [], + "panels": [ + { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "hide": false, + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time_sec,\n avg(value) as value,\n hostname as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'logins.count' AND\n hostname IN($host)\nGROUP BY 1, 3\nORDER BY 1", + "refId": "A", + "target": "" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time_sec,\n min(value) as value,\n 'total avg' as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'logins.count'\nGROUP BY 1\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": "1h", + "title": "Average logins / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL}", + "fill": 2, + "gridPos": { + "h": 18, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time_sec,\n avg(value) as value,\n 'started' as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'payment.started'\nGROUP BY 1, 3\nORDER BY 1", + "refId": "A", + "target": "" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time_sec,\n avg(value) as value,\n 'ended' as \"metric\"\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'payment.ended'\nGROUP BY 1, 3\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": "1h", + "title": "Average payments started/ended / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time_sec,\n max(value) as value,\n hostname as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'cpu' AND\n hostname IN($host)\nGROUP BY 1, 3\nORDER BY 1", + "refId": "A", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": "1h", + "title": "Max CPU / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "columns": [], + "datasource": "${DS_MYSQL}", + "fontSize": "100%", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 6, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "date" + }, + { + "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": "", + "format": "table", + "rawSql": "SELECT createdAt as Time, source, datacenter, hostname, value FROM grafana_metric WHERE hostname in($host)", + "refId": "A", + "target": "" + } + ], + "timeShift": "1h", + "title": "Values", + "transform": "table", + "type": "table" + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [ + "fake-data-gen", + "mysql" + ], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "${DS_MYSQL}", + "hide": 0, + "includeAll": false, + "label": "Datacenter", + "multi": false, + "name": "datacenter", + "options": [], + "query": "SELECT DISTINCT datacenter FROM grafana_metric", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": {}, + "datasource": "${DS_MYSQL}", + "hide": 0, + "includeAll": true, + "label": "Hostname", + "multi": true, + "name": "host", + "options": [], + "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 5, + "auto_min": "10s", + "current": { + "selected": true, + "text": "1m", + "value": "1m" + }, + "hide": 0, + "label": "Summarize", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": true, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Grafana Fake Data Gen - MySQL", + "uid": "DGsCac3kz", + "version": 6 +} \ No newline at end of file diff --git a/docker/blocks/postgres/dashboard.json b/docker/blocks/postgres/dashboard.json new file mode 100644 index 00000000000..77b0ceac624 --- /dev/null +++ b/docker/blocks/postgres/dashboard.json @@ -0,0 +1,547 @@ +{ + "__inputs": [ + { + "name": "DS_POSTGRESQL", + "label": "PostgreSQL", + "description": "", + "type": "datasource", + "pluginId": "postgres", + "pluginName": "PostgreSQL" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "postgres", + "name": "PostgreSQL", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "A dashboard visualizing data generated from grafana/fake-data-gen", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1518601837383, + "links": [], + "panels": [ + { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRESQL}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "hide": false, + "rawSql": "SELECT\n $__timeGroup(\"createdAt\",'$summarize'),\n avg(value) as \"value\",\n hostname as \"metric\"\nFROM \n grafana_metric\nWHERE\n $__timeFilter(\"createdAt\") AND\n measurement = 'logins.count' AND\n hostname IN($host)\nGROUP BY time, metric\nORDER BY time", + "refId": "A", + "target": "" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(\"createdAt\",'$summarize'),\n min(value) as \"value\",\n 'total avg' as \"metric\"\nFROM \n grafana_metric\nWHERE\n $__timeFilter(\"createdAt\") AND\n measurement = 'logins.count'\nGROUP BY time", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Average logins / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRESQL}", + "fill": 2, + "gridPos": { + "h": 18, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(\"createdAt\",'$summarize'),\n avg(value) as \"value\",\n 'started' as \"metric\"\nFROM \n grafana_metric\nWHERE\n $__timeFilter(\"createdAt\") AND\n measurement = 'payment.started'\nGROUP BY time, metric\nORDER BY time", + "refId": "A", + "target": "" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(\"createdAt\",'$summarize'),\n avg(value) as \"value\",\n 'ended' as \"metric\"\nFROM \n grafana_metric\nWHERE\n $__timeFilter(\"createdAt\") AND\n measurement = 'payment.ended'\nGROUP BY time, metric\nORDER BY time", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Average payments started/ended / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRESQL}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(\"createdAt\",'$summarize'),\n max(value) as \"value\",\n hostname as \"metric\"\nFROM \n grafana_metric\nWHERE\n $__timeFilter(\"createdAt\") AND\n measurement = 'cpu' AND\n hostname IN($host)\nGROUP BY time, metric\nORDER BY time", + "refId": "A", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Max CPU / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "columns": [], + "datasource": "${DS_POSTGRESQL}", + "fontSize": "100%", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 6, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "date" + }, + { + "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": "", + "format": "table", + "rawSql": "SELECT \"createdAt\" as \"Time\", source, datacenter, hostname, value FROM grafana_metric WHERE hostname in($host)", + "refId": "A", + "target": "" + } + ], + "title": "Values", + "transform": "table", + "type": "table" + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [ + "fake-data-gen", + "postgres" + ], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "${DS_POSTGRESQL}", + "hide": 0, + "includeAll": false, + "label": "Datacenter", + "multi": false, + "name": "datacenter", + "options": [], + "query": "SELECT DISTINCT datacenter FROM grafana_metric", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": {}, + "datasource": "${DS_POSTGRESQL}", + "hide": 0, + "includeAll": true, + "label": "Hostname", + "multi": true, + "name": "host", + "options": [], + "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 5, + "auto_min": "10s", + "current": { + "text": "1m", + "value": "1m" + }, + "hide": 0, + "label": "Summarize", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": true, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Grafana Fake Data Gen - PostgreSQL", + "uid": "JYola5qzz", + "version": 1 +} \ No newline at end of file From b9d572bdec3cb19418d5665bb41c28b4d5b036d5 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 16 Feb 2018 11:45:53 +0100 Subject: [PATCH 58/73] teams: adds some validation to the API --- pkg/api/team_members.go | 17 +++++++++++++-- pkg/models/team.go | 5 +++-- pkg/services/sqlstore/team.go | 35 +++++++++++++++++++++++++----- pkg/services/sqlstore/team_test.go | 9 +++++--- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 59dfc20b791..8586ac04fdb 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -29,9 +29,14 @@ func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { - if err == m.ErrTeamMemberAlreadyAdded { - return ApiError(400, "User is already added to this team", err) + if err == m.ErrTeamNotFound { + return ApiError(404, "Team not found", nil) } + + if err == m.ErrTeamMemberAlreadyAdded { + return ApiError(400, "User is already added to this team", nil) + } + return ApiError(500, "Failed to add Member to Team", err) } @@ -43,6 +48,14 @@ func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response { // DELETE /api/teams/:teamId/members/:userId func RemoveTeamMember(c *middleware.Context) Response { if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil { + if err == m.ErrTeamNotFound { + return ApiError(404, "Team not found", nil) + } + + if err == m.ErrTeamMemberNotFound { + return ApiError(404, "Team member not found", nil) + } + return ApiError(500, "Failed to remove Member from Team", err) } return ApiSuccess("Team Member removed") diff --git a/pkg/models/team.go b/pkg/models/team.go index f789f125aa1..9c679a13394 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -7,8 +7,9 @@ import ( // Typed errors var ( - ErrTeamNotFound = errors.New("Team not found") - ErrTeamNameTaken = errors.New("Team name is taken") + ErrTeamNotFound = errors.New("Team not found") + ErrTeamNameTaken = errors.New("Team name is taken") + ErrTeamMemberNotFound = errors.New("Team member not found") ) // Team model diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index ecb34ad927b..d238301c7ce 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -78,11 +78,12 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error { }) } +// DeleteTeam will delete a team, its member and any permissions connected to the team func DeleteTeam(cmd *m.DeleteTeamCommand) error { return inTransaction(func(sess *DBSession) error { - if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.Id); err != nil { + if teamExists, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil { return err - } else if len(res) != 1 { + } else if !teamExists { return m.ErrTeamNotFound } @@ -102,6 +103,16 @@ func DeleteTeam(cmd *m.DeleteTeamCommand) error { }) } +func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) { + if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil { + return false, err + } else if len(res) != 1 { + return false, nil + } + + return true, nil +} + func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession) (bool, error) { var team m.Team exists, err := sess.Where("org_id=? and name=?", orgId, name).Get(&team) @@ -190,6 +201,7 @@ func GetTeamById(query *m.GetTeamByIdQuery) error { return nil } +// GetTeamsByUser is used by the Guardian when checking a users' permissions func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { query.Result = make([]*m.Team, 0) @@ -205,6 +217,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { return nil } +// AddTeamMember adds a user to a team func AddTeamMember(cmd *m.AddTeamMemberCommand) error { return inTransaction(func(sess *DBSession) error { if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId); err != nil { @@ -213,9 +226,9 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error { return m.ErrTeamMemberAlreadyAdded } - if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.TeamId); err != nil { + if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil { return err - } else if len(res) != 1 { + } else if !teamExists { return m.ErrTeamNotFound } @@ -232,18 +245,30 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error { }) } +// RemoveTeamMember removes a member from a team func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error { return inTransaction(func(sess *DBSession) error { + if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil { + return err + } else if !teamExists { + return m.ErrTeamNotFound + } + var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?" - _, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId) + res, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId) if err != nil { return err } + rows, err := res.RowsAffected() + if rows == 0 { + return m.ErrTeamMemberNotFound + } return err }) } +// GetTeamMembers return a list of members for the specified team func GetTeamMembers(query *m.GetTeamMembersQuery) error { query.Result = make([]*m.TeamMemberDTO, 0) sess := x.Table("team_member") diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index fb76c3fa9d6..f136411eeba 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -84,13 +84,16 @@ func TestTeamCommandsAndQueries(t *testing.T) { }) Convey("Should be able to remove users from a group", func() { + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]}) + So(err, ShouldBeNil) + err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]}) So(err, ShouldBeNil) - q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id} - err = GetTeamMembers(q1) + q2 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: group1.Result.Id} + err = GetTeamMembers(q2) So(err, ShouldBeNil) - So(len(q1.Result), ShouldEqual, 0) + So(len(q2.Result), ShouldEqual, 0) }) Convey("Should be able to remove a group with users and permissions", func() { From 529fc022d5a3ac5060c84f0ee2577f82aedc7fbd Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 16 Feb 2018 11:46:36 +0100 Subject: [PATCH 59/73] db: reduce name column size in dashboard_provisoning ref #10931 --- pkg/services/sqlstore/migrations/dashboard_mig.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index df561041b7c..7641dedcde5 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -183,7 +183,7 @@ func addDashboardMigration(mg *Migrator) { Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "dashboard_id", Type: DB_BigInt, Nullable: true}, - {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "name", Type: DB_NVarchar, Length: 150, Nullable: false}, {Name: "external_id", Type: DB_Text, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, }, @@ -200,7 +200,7 @@ func addDashboardMigration(mg *Migrator) { Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "dashboard_id", Type: DB_BigInt, Nullable: true}, - {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "name", Type: DB_NVarchar, Length: 150, Nullable: false}, {Name: "external_id", Type: DB_Text, Nullable: false}, {Name: "updated", Type: DB_Int, Default: "0", Nullable: false}, }, From 5e33a52b69f1f68c2419423f4bfeb445a69c614d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Feb 2018 13:26:04 +0100 Subject: [PATCH 60/73] updated version to v5-beta3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8cd0468b9c8..645982e2b4c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.0.0-beta2", + "version": "5.0.0-beta3", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From 5323971c21b13be7d8212fe44baed1761cd6061e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Feb 2018 13:56:04 +0100 Subject: [PATCH 61/73] refactoring: alert rule query refactoring (#10941) --- pkg/api/alerting.go | 70 ++----------------- pkg/models/alert.go | 18 ++++- pkg/services/sqlstore/alert.go | 53 ++++++++------ pkg/services/sqlstore/alert_test.go | 12 ++-- pkg/services/sqlstore/sqlbuilder.go | 16 +++++ .../AlertRuleList/AlertRuleList.tsx | 21 ++---- .../__snapshots__/AlertRuleList.jest.tsx.snap | 1 - public/app/stores/AlertListStore/AlertRule.ts | 1 - 8 files changed, 77 insertions(+), 115 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 16f5f7ceb6f..a8bad638d43 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -52,6 +52,7 @@ func GetAlerts(c *middleware.Context) Response { DashboardId: c.QueryInt64("dashboardId"), PanelId: c.QueryInt64("panelId"), Limit: c.QueryInt64("limit"), + User: c.SignedInUser, } states := c.QueryStrings("state") @@ -63,74 +64,11 @@ func GetAlerts(c *middleware.Context) Response { return ApiError(500, "List alerts failed", err) } - alertDTOs, resp := transformToDTOs(query.Result, c) - if resp != nil { - return resp + for _, alert := range query.Result { + alert.Url = models.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug) } - return Json(200, alertDTOs) -} - -func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.AlertRule, Response) { - if len(alerts) == 0 { - return []*dtos.AlertRule{}, nil - } - - dashboardIds := make([]int64, 0) - alertDTOs := make([]*dtos.AlertRule, 0) - for _, alert := range alerts { - dashboardIds = append(dashboardIds, alert.DashboardId) - alertDTOs = append(alertDTOs, &dtos.AlertRule{ - Id: alert.Id, - DashboardId: alert.DashboardId, - PanelId: alert.PanelId, - Name: alert.Name, - Message: alert.Message, - State: alert.State, - NewStateDate: alert.NewStateDate, - ExecutionError: alert.ExecutionError, - EvalData: alert.EvalData, - }) - } - - dashboardsQuery := models.GetDashboardsQuery{ - DashboardIds: dashboardIds, - } - - if err := bus.Dispatch(&dashboardsQuery); err != nil { - return nil, ApiError(500, "List alerts failed", err) - } - - //TODO: should be possible to speed this up with lookup table - for _, alert := range alertDTOs { - for _, dash := range dashboardsQuery.Result { - if alert.DashboardId == dash.Id { - alert.Url = dash.GenerateUrl() - break - } - } - } - - permissionsQuery := models.GetDashboardPermissionsForUserQuery{ - DashboardIds: dashboardIds, - OrgId: c.OrgId, - UserId: c.SignedInUser.UserId, - OrgRole: c.SignedInUser.OrgRole, - } - - if err := bus.Dispatch(&permissionsQuery); err != nil { - return nil, ApiError(500, "List alerts failed", err) - } - - for _, alert := range alertDTOs { - for _, perm := range permissionsQuery.Result { - if alert.DashboardId == perm.DashboardId { - alert.CanEdit = perm.Permission > 1 - } - } - } - - return alertDTOs, nil + return Json(200, query.Result) } // POST /api/alerts/test diff --git a/pkg/models/alert.go b/pkg/models/alert.go index b378c5cf90f..6039ecef6ba 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -166,8 +166,9 @@ type GetAlertsQuery struct { DashboardId int64 PanelId int64 Limit int64 + User *SignedInUser - Result []*Alert + Result []*AlertListItemDTO } type GetAllAlertsQuery struct { @@ -187,6 +188,21 @@ type GetAlertStatesForDashboardQuery struct { Result []*AlertStateInfoDTO } +type AlertListItemDTO struct { + Id int64 `json:"id"` + DashboardId int64 `json:"dashboardId"` + DashboardUid string `json:"dashboardUid"` + DashboardSlug string `json:"dashboardSlug"` + PanelId int64 `json:"panelId"` + Name string `json:"name"` + State AlertStateType `json:"state"` + NewStateDate time.Time `json:"newStateDate"` + EvalDate time.Time `json:"evalDate"` + EvalData *simplejson.Json `json:"evalData"` + ExecutionError string `json:"executionError"` + Url string `json:"url"` +} + type AlertStateInfoDTO struct { Id int64 `json:"id"` DashboardId int64 `json:"dashboardId"` diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 96af8bc49ee..8c751f0cada 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -61,52 +61,61 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *DBSession) erro } func HandleAlertsQuery(query *m.GetAlertsQuery) error { - var sql bytes.Buffer - params := make([]interface{}, 0) + builder := SqlBuilder{} - sql.WriteString(`SELECT * - from alert - `) + builder.Write(`SELECT + alert.id, + alert.dashboard_id, + alert.panel_id, + alert.name, + alert.state, + alert.new_state_date, + alert.eval_date, + alert.execution_error, + dashboard.uid as dashboard_uid, + dashboard.slug as dashboard_slug + FROM alert + INNER JOIN dashboard on dashboard.id = alert.dashboard_id `) - sql.WriteString(`WHERE org_id = ?`) - params = append(params, query.OrgId) + builder.Write(`WHERE alert.org_id = ?`, query.OrgId) if query.DashboardId != 0 { - sql.WriteString(` AND dashboard_id = ?`) - params = append(params, query.DashboardId) + builder.Write(` AND alert.dashboard_id = ?`, query.DashboardId) } if query.PanelId != 0 { - sql.WriteString(` AND panel_id = ?`) - params = append(params, query.PanelId) + builder.Write(` AND alert.panel_id = ?`, query.PanelId) } if len(query.State) > 0 && query.State[0] != "all" { - sql.WriteString(` AND (`) + builder.Write(` AND (`) for i, v := range query.State { if i > 0 { - sql.WriteString(" OR ") + builder.Write(" OR ") } if strings.HasPrefix(v, "not_") { - sql.WriteString("state <> ? ") + builder.Write("state <> ? ") v = strings.TrimPrefix(v, "not_") } else { - sql.WriteString("state = ? ") + builder.Write("state = ? ") } - params = append(params, v) + builder.AddParams(v) } - sql.WriteString(")") + builder.Write(")") } - sql.WriteString(" ORDER BY name ASC") + if query.User.OrgRole != m.ROLE_ADMIN { + builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_EDIT) + } + + builder.Write(" ORDER BY name ASC") if query.Limit != 0 { - sql.WriteString(" LIMIT ?") - params = append(params, query.Limit) + builder.Write(" LIMIT ?", query.Limit) } - alerts := make([]*m.Alert, 0) - if err := x.SQL(sql.String(), params...).Find(&alerts); err != nil { + alerts := make([]*m.AlertListItemDTO, 0) + if err := x.SQL(builder.GetSqlString(), builder.params...).Find(&alerts); err != nil { return err } diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index 7b27f5b9ca4..26909c59233 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -71,15 +71,13 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Can read properties", func() { - alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1} + alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&alertQuery) alert := alertQuery.Result[0] So(err2, ShouldBeNil) So(alert.Name, ShouldEqual, "Alerting title") - So(alert.Message, ShouldEqual, "Alerting message") So(alert.State, ShouldEqual, "pending") - So(alert.Frequency, ShouldEqual, 1) }) Convey("Alerts with same dashboard id and panel id should update", func() { @@ -100,7 +98,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Alerts should be updated", func() { - query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1} + query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&query) So(err2, ShouldBeNil) @@ -149,7 +147,7 @@ func TestAlertingDataAccess(t *testing.T) { Convey("Should save 3 dashboards", func() { So(err, ShouldBeNil) - queryForDashboard := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1} + queryForDashboard := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&queryForDashboard) So(err2, ShouldBeNil) @@ -163,7 +161,7 @@ func TestAlertingDataAccess(t *testing.T) { err = SaveAlerts(&cmd) Convey("should delete the missing alert", func() { - query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1} + query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&query) So(err2, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -198,7 +196,7 @@ func TestAlertingDataAccess(t *testing.T) { So(err, ShouldBeNil) Convey("Alerts should be removed", func() { - query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1} + query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&query) So(testDash.Id, ShouldEqual, 1) diff --git a/pkg/services/sqlstore/sqlbuilder.go b/pkg/services/sqlstore/sqlbuilder.go index b38bd693e66..b42c7926203 100644 --- a/pkg/services/sqlstore/sqlbuilder.go +++ b/pkg/services/sqlstore/sqlbuilder.go @@ -12,6 +12,22 @@ type SqlBuilder struct { params []interface{} } +func (sb *SqlBuilder) Write(sql string, params ...interface{}) { + sb.sql.WriteString(sql) + + if len(params) > 0 { + sb.params = append(sb.params, params...) + } +} + +func (sb *SqlBuilder) GetSqlString() string { + return sb.sql.String() +} + +func (sb *SqlBuilder) AddParams(params ...interface{}) { + sb.params = append(sb.params, params...) +} + func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permission m.PermissionType) { if user.OrgRole == m.ROLE_ADMIN { diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx index 6fb6e3b7d8f..9ecb9a177d7 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx @@ -147,8 +147,7 @@ export class AlertRuleItem extends React.Component {
- {rule.canEdit && {this.renderText(rule.name)}} - {!rule.canEdit && {this.renderText(rule.name)}} + {this.renderText(rule.name)}
{this.renderText(rule.stateText)} @@ -163,24 +162,12 @@ export class AlertRuleItem extends React.Component { className="btn btn-small btn-inverse alert-list__btn width-2" title="Pausing an alert rule prevents it from executing" onClick={this.toggleState} - disabled={!rule.canEdit} > - {rule.canEdit && ( - - - - )} - {!rule.canEdit && ( - - )} + + +
); diff --git a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap index 0914f050a0f..f408f6409be 100644 --- a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap +++ b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap @@ -82,7 +82,6 @@ exports[`AlertRuleList should render 1 rule 1`] = ` >