diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index a95a5dc3d4a..2065cc3b36a 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -54,15 +54,16 @@ type Query struct { } type FindPersistedDashboardsQuery struct { - Title string - OrgId int64 - SignedInUser *models.SignedInUser - IsStarred bool - DashboardIds []int64 - Type string - FolderId int64 - Tags []string - Limit int + Title string + OrgId int64 + SignedInUser *models.SignedInUser + IsStarred bool + DashboardIds []int64 + Type string + FolderId int64 + Tags []string + ExpandedFolders []int64 + Limit int Result HitList } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index fcc49799def..ce4c74ac3df 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -1,9 +1,6 @@ package sqlstore import ( - "bytes" - "fmt" - "strings" "time" "github.com/grafana/grafana/pkg/bus" @@ -189,77 +186,39 @@ type DashboardSearchProjection struct { } func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { - var sql bytes.Buffer - params := make([]interface{}, 0) limit := query.Limit if limit == 0 { limit = 1000 } - sql.WriteString(` - SELECT - dashboard.id, - dashboard.title, - dashboard.slug, - dashboard_tag.term, - dashboard.is_folder, - dashboard.folder_id, - folder.slug as folder_slug, - folder.title as folder_title - FROM `) + sb := NewSearchBuilder(query.SignedInUser, limit). + WithTags(query.Tags). + WithDashboardIdsIn(query.DashboardIds) - // add tags filter - if len(query.Tags) > 0 { - sql.WriteString( - `( - SELECT - dashboard.id FROM dashboard - LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id - `) - if query.IsStarred { - sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") - } - - sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(query.Tags)-1) + `) AND `) - for _, tag := range query.Tags { - params = append(params, tag) - } - params = createSearchWhereClause(query, &sql, params) - fmt.Printf("params2 %v", params) - - // this ends the inner select (tag filtered part) - sql.WriteString(` - GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ? - LIMIT ?) as ids - INNER JOIN dashboard on ids.id = dashboard.id - `) - - params = append(params, len(query.Tags)) - params = append(params, limit) - } else { - sql.WriteString(`( SELECT dashboard.id FROM dashboard `) - if query.IsStarred { - sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") - } - sql.WriteString(` WHERE `) - params = createSearchWhereClause(query, &sql, params) - - sql.WriteString(` - LIMIT ?) as ids - INNER JOIN dashboard on ids.id = dashboard.id - `) - params = append(params, limit) + if query.IsStarred { + sb.IsStarred() } - sql.WriteString(` - LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id - LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`) + if len(query.Title) > 0 { + sb.WithTitle(query.Title) + } - sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 5000")) + if len(query.Type) > 0 { + sb.WithType(query.Type) + } + + if query.FolderId > 0 { + sb.WithFolderId(query.FolderId) + } + + if len(query.ExpandedFolders) > 0 { + sb.WithExpandedFolders(query.ExpandedFolders) + } var res []DashboardSearchProjection - err := x.Sql(sql.String(), params...).Find(&res) + sql, params := sb.ToSql() + err := x.Sql(sql, params...).Find(&res) if err != nil { return nil, err } @@ -267,61 +226,6 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear return res, nil } -func createSearchWhereClause(query *search.FindPersistedDashboardsQuery, sql *bytes.Buffer, params []interface{}) []interface{} { - sql.WriteString(` dashboard.org_id=?`) - params = append(params, query.SignedInUser.OrgId) - - if query.IsStarred { - sql.WriteString(` AND star.user_id=?`) - params = append(params, query.SignedInUser.UserId) - } - - if len(query.DashboardIds) > 0 { - sql.WriteString(` AND dashboard.id IN (?` + strings.Repeat(",?", len(query.DashboardIds)-1) + `)`) - for _, dashboardId := range query.DashboardIds { - params = append(params, dashboardId) - } - } - - if query.SignedInUser.OrgRole != m.ROLE_ADMIN { - allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 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 user_group_member as ugm on ugm.user_group_id = da.user_group_id - LEFT JOIN org_user ou on ou.role = da.role - WHERE - d.has_acl = 1 and - (da.user_id = ? or ugm.user_id = ? or ou.id is not null) - and d.org_id = ? - ) - )` - - sql.WriteString(allowedDashboardsSubQuery) - params = append(params, query.SignedInUser.UserId, query.SignedInUser.UserId, query.SignedInUser.OrgId) - } - - if len(query.Title) > 0 { - sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") - params = append(params, "%"+query.Title+"%") - } - - if len(query.Type) > 0 && query.Type == "dash-folder" { - sql.WriteString(" AND dashboard.is_folder = 1") - } - - if len(query.Type) > 0 && query.Type == "dash-db" { - sql.WriteString(" AND dashboard.is_folder = 0") - } - - if query.FolderId > 0 { - sql.WriteString(" AND dashboard.folder_id = ?") - params = append(params, query.FolderId) - } - - return params -} - func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { res, err := findDashboards(query) if err != nil { diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index e0473b0e38f..983b4ca1814 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -382,6 +382,19 @@ func TestDashboardDataAccess(t *testing.T) { currentUser := createUser("viewer", "Viewer", false) + Convey("and one folder is expanded, the other collapsed", func() { + Convey("should return dashboards in root and expanded folder", func() { + query := &search.FindPersistedDashboardsQuery{ExpandedFolders: []int64{folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 4) + So(query.Result[0].Id, ShouldEqual, folder1.Id) + So(query.Result[1].Id, ShouldEqual, folder2.Id) + So(query.Result[2].Id, ShouldEqual, childDash1.Id) + So(query.Result[3].Id, ShouldEqual, dashInRoot.Id) + }) + }) + Convey("and acl is set for one dashboard folder", func() { var otherUser int64 = 999 updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT) diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go new file mode 100644 index 00000000000..f58858d5347 --- /dev/null +++ b/pkg/services/sqlstore/search_builder.go @@ -0,0 +1,228 @@ +package sqlstore + +import ( + "bytes" + "strings" + + m "github.com/grafana/grafana/pkg/models" +) + +// SearchBuilder is a builder/object mother that builds a dashboard search query +type SearchBuilder struct { + tags []string + isStarred bool + limit int + signedInUser *m.SignedInUser + whereDashboardIdsIn []int64 + whereTitle string + whereTypeFolder bool + whereTypeDash bool + whereFolderId int64 + expandedFolders []int64 + sql bytes.Buffer + params []interface{} +} + +func NewSearchBuilder(signedInUser *m.SignedInUser, limit int) *SearchBuilder { + searchBuilder := &SearchBuilder{ + signedInUser: signedInUser, + limit: limit, + } + + return searchBuilder +} + +func (sb *SearchBuilder) WithTags(tags []string) *SearchBuilder { + if len(tags) > 0 { + sb.tags = tags + } + + return sb +} + +func (sb *SearchBuilder) IsStarred() *SearchBuilder { + sb.isStarred = true + + return sb +} + +func (sb *SearchBuilder) WithDashboardIdsIn(ids []int64) *SearchBuilder { + if len(ids) > 0 { + sb.whereDashboardIdsIn = ids + } + + return sb +} + +func (sb *SearchBuilder) WithTitle(title string) *SearchBuilder { + sb.whereTitle = title + + return sb +} + +func (sb *SearchBuilder) WithType(queryType string) *SearchBuilder { + if len(queryType) > 0 && queryType == "dash-folder" { + sb.whereTypeFolder = true + } + + if len(queryType) > 0 && queryType == "dash-db" { + sb.whereTypeDash = true + } + + return sb +} + +func (sb *SearchBuilder) WithFolderId(folderId int64) *SearchBuilder { + sb.whereFolderId = folderId + + return sb +} + +func (sb *SearchBuilder) WithExpandedFolders(expandedFolders []int64) *SearchBuilder { + sb.expandedFolders = expandedFolders + return sb +} + +// ToSql builds the sql and returns it as a string, together with the params. +func (sb *SearchBuilder) ToSql() (string, []interface{}) { + sb.params = make([]interface{}, 0) + + sb.buildSelect() + + if len(sb.tags) > 0 { + sb.buildTagQuery() + } else { + sb.buildMainQuery() + } + + sb.sql.WriteString(` + LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id + LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`) + + sb.sql.WriteString(" ORDER BY dashboard.title ASC LIMIT 5000") + + return sb.sql.String(), sb.params +} + +func (sb *SearchBuilder) buildSelect() { + sb.sql.WriteString( + `SELECT + dashboard.id, + dashboard.title, + dashboard.slug, + dashboard_tag.term, + dashboard.is_folder, + dashboard.folder_id, + folder.slug as folder_slug, + folder.title as folder_title + FROM `) +} + +func (sb *SearchBuilder) buildTagQuery() { + sb.sql.WriteString( + `( + SELECT + dashboard.id FROM dashboard + LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id + `) + + if sb.isStarred { + sb.sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") + } + + sb.sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(sb.tags)-1) + `) AND `) + for _, tag := range sb.tags { + sb.params = append(sb.params, tag) + } + + sb.buildSearchWhereClause() + + // this ends the inner select (tag filtered part) + sb.sql.WriteString(` + GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ? + LIMIT ?) as ids + INNER JOIN dashboard on ids.id = dashboard.id + `) + + sb.params = append(sb.params, len(sb.tags)) + sb.params = append(sb.params, sb.limit) +} + +func (sb *SearchBuilder) buildMainQuery() { + sb.sql.WriteString(`( SELECT dashboard.id FROM dashboard `) + + if sb.isStarred { + sb.sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") + } + + sb.sql.WriteString(` WHERE `) + sb.buildSearchWhereClause() + + sb.sql.WriteString(` + LIMIT ?) as ids + INNER JOIN dashboard on ids.id = dashboard.id + `) + sb.params = append(sb.params, sb.limit) +} + +func (sb *SearchBuilder) buildSearchWhereClause() { + sb.sql.WriteString(` dashboard.org_id=?`) + sb.params = append(sb.params, sb.signedInUser.OrgId) + + if sb.isStarred { + sb.sql.WriteString(` AND star.user_id=?`) + sb.params = append(sb.params, sb.signedInUser.UserId) + } + + if len(sb.whereDashboardIdsIn) > 0 { + sb.sql.WriteString(` AND dashboard.id IN (?` + strings.Repeat(",?", len(sb.whereDashboardIdsIn)-1) + `)`) + for _, dashboardId := range sb.whereDashboardIdsIn { + sb.params = append(sb.params, dashboardId) + } + } + + if sb.signedInUser.OrgRole != m.ROLE_ADMIN { + allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 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 user_group_member as ugm on ugm.user_group_id = da.user_group_id + LEFT JOIN org_user ou on ou.role = da.role + WHERE + d.has_acl = 1 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) + } + + if len(sb.whereTitle) > 0 { + sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") + sb.params = append(sb.params, "%"+sb.whereTitle+"%") + } + + if sb.whereTypeFolder { + sb.sql.WriteString(" AND dashboard.is_folder = 1") + } + + if sb.whereTypeDash { + sb.sql.WriteString(" AND dashboard.is_folder = 0") + } + + if sb.whereFolderId > 0 { + sb.sql.WriteString(" AND dashboard.folder_id = ?") + sb.params = append(sb.params, sb.whereFolderId) + } + + if len(sb.expandedFolders) > 0 { + sb.sql.WriteString(` AND (dashboard.folder_id IN (?` + strings.Repeat(",?", len(sb.expandedFolders)-1) + `) `) + sb.sql.WriteString(` OR dashboard.folder_id IS NULL OR dashboard.folder_id = 0)`) + + for _, ef := range sb.expandedFolders { + sb.params = append(sb.params, ef) + } + } +} diff --git a/pkg/services/sqlstore/search_builder_test.go b/pkg/services/sqlstore/search_builder_test.go new file mode 100644 index 00000000000..32ccbc583f5 --- /dev/null +++ b/pkg/services/sqlstore/search_builder_test.go @@ -0,0 +1,37 @@ +package sqlstore + +import ( + "testing" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + . "github.com/smartystreets/goconvey/convey" +) + +func TestSearchBuilder(t *testing.T) { + dialect = migrator.NewDialect("sqlite3") + + Convey("Testing building a search", t, func() { + signedInUser := &m.SignedInUser{ + OrgId: 1, + UserId: 1, + } + sb := NewSearchBuilder(signedInUser, 1000) + + Convey("When building a normal search", func() { + sql, params := sb.IsStarred().WithTitle("test").ToSql() + So(sql, ShouldStartWith, "SELECT") + So(sql, ShouldContainSubstring, "INNER JOIN dashboard on ids.id = dashboard.id") + So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000") + So(len(params), ShouldBeGreaterThan, 0) + }) + + Convey("When building a search with tag filter", func() { + sql, params := sb.WithTags([]string{"tag1", "tag2"}).ToSql() + So(sql, ShouldStartWith, "SELECT") + So(sql, ShouldContainSubstring, "LEFT OUTER JOIN dashboard_tag") + So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000") + So(len(params), ShouldBeGreaterThan, 0) + }) + }) +}