mirror of https://github.com/grafana/grafana.git
				
				
				
			Search: Improvements for starred dashboard search (#64758)
* improvements for starred dashboard search * fix workflows for the case when no dashboards are starred * PR feedback (don't query DB if starred dashboards and requested but no starred IDs are found) and linting * return empty list not null in case of no starred dashboards * return empty list not null in case of no starred dashboards pt 2 * return empty list not null in case of no starred dashboards pt 3
This commit is contained in:
		
							parent
							
								
									8617ad688d
								
							
						
					
					
						commit
						f966045129
					
				|  | @ -952,10 +952,6 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F | ||||||
| 		filters = append(filters, searchstore.DashboardIDFilter{IDs: query.DashboardIds}) | 		filters = append(filters, searchstore.DashboardIDFilter{IDs: query.DashboardIds}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if query.IsStarred { |  | ||||||
| 		filters = append(filters, searchstore.StarredFilter{UserId: query.SignedInUser.UserID}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if len(query.Title) > 0 { | 	if len(query.Title) > 0 { | ||||||
| 		filters = append(filters, searchstore.TitleFilter{Dialect: d.store.GetDialect(), Title: query.Title}) | 		filters = append(filters, searchstore.TitleFilter{Dialect: d.store.GetDialect(), Title: query.Title}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -20,8 +20,6 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/services/search/model" | 	"github.com/grafana/grafana/pkg/services/search/model" | ||||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore" | 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore/searchstore" | 	"github.com/grafana/grafana/pkg/services/sqlstore/searchstore" | ||||||
| 	"github.com/grafana/grafana/pkg/services/star" |  | ||||||
| 	"github.com/grafana/grafana/pkg/services/star/starimpl" |  | ||||||
| 	"github.com/grafana/grafana/pkg/services/tag/tagimpl" | 	"github.com/grafana/grafana/pkg/services/tag/tagimpl" | ||||||
| 	"github.com/grafana/grafana/pkg/services/user" | 	"github.com/grafana/grafana/pkg/services/user" | ||||||
| 	"github.com/grafana/grafana/pkg/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
|  | @ -35,11 +33,9 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { | ||||||
| 	var cfg *setting.Cfg | 	var cfg *setting.Cfg | ||||||
| 	var savedFolder, savedDash, savedDash2 *dashboards.Dashboard | 	var savedFolder, savedDash, savedDash2 *dashboards.Dashboard | ||||||
| 	var dashboardStore dashboards.Store | 	var dashboardStore dashboards.Store | ||||||
| 	var starService star.Service |  | ||||||
| 
 | 
 | ||||||
| 	setup := func() { | 	setup := func() { | ||||||
| 		sqlStore, cfg = db.InitTestDBwithCfg(t) | 		sqlStore, cfg = db.InitTestDBwithCfg(t) | ||||||
| 		starService = starimpl.ProvideService(sqlStore, cfg) |  | ||||||
| 		quotaService := quotatest.New(false, nil) | 		quotaService := quotatest.New(false, nil) | ||||||
| 		var err error | 		var err error | ||||||
| 		dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) | 		dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) | ||||||
|  | @ -455,39 +451,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { | ||||||
| 		require.Equal(t, len(hit2.Tags), 1) | 		require.Equal(t, len(hit2.Tags), 1) | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	t.Run("Should be able to find starred dashboards", func(t *testing.T) { |  | ||||||
| 		setup() |  | ||||||
| 		starredDash := insertTestDashboard(t, dashboardStore, "starred dash", 1, 0, false) |  | ||||||
| 		err := starService.Add(context.Background(), &star.StarDashboardCommand{ |  | ||||||
| 			DashboardID: starredDash.ID, |  | ||||||
| 			UserID:      10, |  | ||||||
| 		}) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 		err = starService.Add(context.Background(), &star.StarDashboardCommand{ |  | ||||||
| 			DashboardID: savedDash.ID, |  | ||||||
| 			UserID:      1, |  | ||||||
| 		}) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 		query := dashboards.FindPersistedDashboardsQuery{ |  | ||||||
| 			SignedInUser: &user.SignedInUser{ |  | ||||||
| 				UserID:  10, |  | ||||||
| 				OrgID:   1, |  | ||||||
| 				OrgRole: org.RoleEditor, |  | ||||||
| 				Permissions: map[int64]map[string][]string{ |  | ||||||
| 					1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			IsStarred: true, |  | ||||||
| 		} |  | ||||||
| 		res, err := dashboardStore.FindDashboards(context.Background(), &query) |  | ||||||
| 
 |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		require.Equal(t, len(res), 1) |  | ||||||
| 		require.Equal(t, res[0].Title, "starred dash") |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	t.Run("Can count dashboards by parent folder", func(t *testing.T) { | 	t.Run("Can count dashboards by parent folder", func(t *testing.T) { | ||||||
| 		setup() | 		setup() | ||||||
| 		// setup() saves one dashboard in the general folder and two in the "savedFolder".
 | 		// setup() saves one dashboard in the general folder and two in the "savedFolder".
 | ||||||
|  |  | ||||||
|  | @ -405,7 +405,6 @@ type FindPersistedDashboardsQuery struct { | ||||||
| 	Title         string | 	Title         string | ||||||
| 	OrgId         int64 | 	OrgId         int64 | ||||||
| 	SignedInUser  *user.SignedInUser | 	SignedInUser  *user.SignedInUser | ||||||
| 	IsStarred     bool |  | ||||||
| 	DashboardIds  []int64 | 	DashboardIds  []int64 | ||||||
| 	DashboardUIDs []string | 	DashboardUIDs []string | ||||||
| 	Type          string | 	Type          string | ||||||
|  |  | ||||||
|  | @ -224,11 +224,19 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool | ||||||
| func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) { | func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) { | ||||||
| 	ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator) | 	ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserID, "id", a.dashboard.ID) | 		id := 0 | ||||||
|  | 		if a.dashboard != nil { | ||||||
|  | 			id = int(a.dashboard.ID) | ||||||
|  | 		} | ||||||
|  | 		a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserID, "id", id) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !ok && err == nil { | 	if !ok && err == nil { | ||||||
| 		a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserID, "id", a.dashboard.ID, "permissions", evaluator.GoString()) | 		id := 0 | ||||||
|  | 		if a.dashboard != nil { | ||||||
|  | 			id = int(a.dashboard.ID) | ||||||
|  | 		} | ||||||
|  | 		a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserID, "id", id, "permissions", evaluator.GoString()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return ok, err | 	return ok, err | ||||||
|  |  | ||||||
|  | @ -345,25 +345,20 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *contextmodel.ReqContext) ([]* | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	starredDashboards := []*dashboards.Dashboard{} | 	if len(starredDashboardResult.UserStars) > 0 { | ||||||
| 	starredDashboardsCounter := 0 | 		var ids []int64 | ||||||
| 	for dashboardId := range starredDashboardResult.UserStars { | 		for id := range starredDashboardResult.UserStars { | ||||||
|  | 			ids = append(ids, id) | ||||||
|  | 		} | ||||||
|  | 		starredDashboards, err := s.dashboardService.GetDashboards(c.Req.Context(), &dashboards.GetDashboardsQuery{DashboardIDs: ids, OrgID: c.OrgID}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
| 		// Set a loose limit to the first 50 starred dashboards found
 | 		// Set a loose limit to the first 50 starred dashboards found
 | ||||||
| 		if starredDashboardsCounter > 50 { | 		if len(starredDashboards) > 50 { | ||||||
| 			break | 			starredDashboards = starredDashboards[:50] | ||||||
| 		} | 		} | ||||||
| 		starredDashboardsCounter++ |  | ||||||
| 		query := &dashboards.GetDashboardQuery{ |  | ||||||
| 			ID:    dashboardId, |  | ||||||
| 			OrgID: c.OrgID, |  | ||||||
| 		} |  | ||||||
| 		queryResult, err := s.dashboardService.GetDashboard(c.Req.Context(), query) |  | ||||||
| 		if err == nil { |  | ||||||
| 			starredDashboards = append(starredDashboards, queryResult) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if len(starredDashboards) > 0 { |  | ||||||
| 		sort.Slice(starredDashboards, func(i, j int) bool { | 		sort.Slice(starredDashboards, func(i, j int) bool { | ||||||
| 			return starredDashboards[i].Title < starredDashboards[j].Title | 			return starredDashboards[i].Title < starredDashboards[j].Title | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
|  | @ -58,10 +58,30 @@ type SearchService struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *SearchService) SearchHandler(ctx context.Context, query *Query) error { | func (s *SearchService) SearchHandler(ctx context.Context, query *Query) error { | ||||||
|  | 	starredQuery := star.GetUserStarsQuery{ | ||||||
|  | 		UserID: query.SignedInUser.UserID, | ||||||
|  | 	} | ||||||
|  | 	staredDashIDs, err := s.starService.GetByUser(ctx, &starredQuery) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// No starred dashboards will be found
 | ||||||
|  | 	if query.IsStarred && len(staredDashIDs.UserStars) == 0 { | ||||||
|  | 		query.Result = model.HitList{} | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// filter by starred dashboard IDs when starred dashboards are requested and no UID or ID filters are specified to improve query performance
 | ||||||
|  | 	if query.IsStarred && len(query.DashboardIds) == 0 && len(query.DashboardUIDs) == 0 { | ||||||
|  | 		for id := range staredDashIDs.UserStars { | ||||||
|  | 			query.DashboardIds = append(query.DashboardIds, id) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	dashboardQuery := dashboards.FindPersistedDashboardsQuery{ | 	dashboardQuery := dashboards.FindPersistedDashboardsQuery{ | ||||||
| 		Title:         query.Title, | 		Title:         query.Title, | ||||||
| 		SignedInUser:  query.SignedInUser, | 		SignedInUser:  query.SignedInUser, | ||||||
| 		IsStarred:     query.IsStarred, |  | ||||||
| 		DashboardUIDs: query.DashboardUIDs, | 		DashboardUIDs: query.DashboardUIDs, | ||||||
| 		DashboardIds:  query.DashboardIds, | 		DashboardIds:  query.DashboardIds, | ||||||
| 		Type:          query.Type, | 		Type:          query.Type, | ||||||
|  | @ -85,11 +105,24 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) error { | ||||||
| 		hits = sortedHits(hits) | 		hits = sortedHits(hits) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := s.setStarredDashboards(ctx, query.SignedInUser.UserID, hits); err != nil { | 	// set starred dashboards
 | ||||||
| 		return err | 	for _, dashboard := range hits { | ||||||
|  | 		if _, ok := staredDashIDs.UserStars[dashboard.ID]; ok { | ||||||
|  | 			dashboard.IsStarred = true | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	query.Result = hits | 	// filter for starred dashboards if requested
 | ||||||
|  | 	if !query.IsStarred { | ||||||
|  | 		query.Result = hits | ||||||
|  | 	} else { | ||||||
|  | 		query.Result = model.HitList{} | ||||||
|  | 		for _, dashboard := range hits { | ||||||
|  | 			if dashboard.IsStarred { | ||||||
|  | 				query.Result = append(query.Result, dashboard) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | @ -106,22 +139,3 @@ func sortedHits(unsorted model.HitList) model.HitList { | ||||||
| 
 | 
 | ||||||
| 	return hits | 	return hits | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func (s *SearchService) setStarredDashboards(ctx context.Context, userID int64, hits []*model.Hit) error { |  | ||||||
| 	query := star.GetUserStarsQuery{ |  | ||||||
| 		UserID: userID, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	res, err := s.starService.GetByUser(ctx, &query) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	iuserstars := res.UserStars |  | ||||||
| 	for _, dashboard := range hits { |  | ||||||
| 		if _, ok := iuserstars[dashboard.ID]; ok { |  | ||||||
| 			dashboard.IsStarred = true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -62,3 +62,39 @@ func TestSearch_SortedResults(t *testing.T) { | ||||||
| 	assert.Equal(t, "BB", query.Result[3].Tags[1]) | 	assert.Equal(t, "BB", query.Result[3].Tags[1]) | ||||||
| 	assert.Equal(t, "EE", query.Result[3].Tags[2]) | 	assert.Equal(t, "EE", query.Result[3].Tags[2]) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestSearch_StarredResults(t *testing.T) { | ||||||
|  | 	ss := startest.NewStarServiceFake() | ||||||
|  | 	db := dbtest.NewFakeDB() | ||||||
|  | 	us := usertest.NewUserServiceFake() | ||||||
|  | 	ds := dashboards.NewFakeDashboardService(t) | ||||||
|  | 	ds.On("SearchDashboards", mock.Anything, mock.AnythingOfType("*dashboards.FindPersistedDashboardsQuery")).Run(func(args mock.Arguments) { | ||||||
|  | 		q := args.Get(1).(*dashboards.FindPersistedDashboardsQuery) | ||||||
|  | 		q.Result = model.HitList{ | ||||||
|  | 			&model.Hit{ID: 1, Title: "A", Type: "dash-db"}, | ||||||
|  | 			&model.Hit{ID: 2, Title: "B", Type: "dash-db"}, | ||||||
|  | 			&model.Hit{ID: 3, Title: "C", Type: "dash-db"}, | ||||||
|  | 		} | ||||||
|  | 	}).Return(nil) | ||||||
|  | 	us.ExpectedSignedInUser = &user.SignedInUser{} | ||||||
|  | 	ss.ExpectedUserStars = &star.GetUserStarsResult{UserStars: map[int64]bool{1: true, 3: true, 4: true}} | ||||||
|  | 	svc := &SearchService{ | ||||||
|  | 		sqlstore:         db, | ||||||
|  | 		starService:      ss, | ||||||
|  | 		dashboardService: ds, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	query := &Query{ | ||||||
|  | 		Limit:        2000, | ||||||
|  | 		IsStarred:    true, | ||||||
|  | 		SignedInUser: &user.SignedInUser{}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := svc.SearchHandler(context.Background(), query) | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 
 | ||||||
|  | 	// Assert only starred dashboards are returned
 | ||||||
|  | 	assert.Equal(t, 2, query.Result.Len()) | ||||||
|  | 	assert.Equal(t, "A", query.Result[0].Title) | ||||||
|  | 	assert.Equal(t, "C", query.Result[1].Title) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -68,16 +68,6 @@ func (f OrgFilter) Where() (string, []interface{}) { | ||||||
| 	return "dashboard.org_id=?", []interface{}{f.OrgId} | 	return "dashboard.org_id=?", []interface{}{f.OrgId} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type StarredFilter struct { |  | ||||||
| 	UserId int64 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (f StarredFilter) Where() (string, []interface{}) { |  | ||||||
| 	return `(SELECT count(*) |  | ||||||
| 			 FROM star |  | ||||||
| 			 WHERE star.dashboard_id = dashboard.id AND star.user_id = ?) > 0`, []interface{}{f.UserId} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type TitleFilter struct { | type TitleFilter struct { | ||||||
| 	Dialect migrator.Dialect | 	Dialect migrator.Dialect | ||||||
| 	Title   string | 	Title   string | ||||||
|  |  | ||||||
|  | @ -52,22 +52,26 @@ func (api *API) GetStars(c *contextmodel.ReqContext) response.Response { | ||||||
| 
 | 
 | ||||||
| 	iuserstars, err := api.starService.GetByUser(c.Req.Context(), &query) | 	iuserstars, err := api.starService.GetByUser(c.Req.Context(), &query) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return response.Error(500, "Failed to get user stars", err) | 		return response.Error(http.StatusInternalServerError, "Failed to get user stars", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	uids := []string{} | 	uids := []string{} | ||||||
| 	for dashboardId := range iuserstars.UserStars { | 	if len(iuserstars.UserStars) > 0 { | ||||||
| 		query := &dashboards.GetDashboardQuery{ | 		var ids []int64 | ||||||
| 			ID:    dashboardId, | 		for id := range iuserstars.UserStars { | ||||||
| 			OrgID: c.OrgID, | 			ids = append(ids, id) | ||||||
|  | 		} | ||||||
|  | 		starredDashboards, err := api.dashboardService.GetDashboards(c.Req.Context(), &dashboards.GetDashboardsQuery{DashboardIDs: ids, OrgID: c.OrgID}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return response.ErrOrFallback(http.StatusInternalServerError, "Failed to fetch dashboards", err) | ||||||
| 		} | 		} | ||||||
| 		queryResult, err := api.dashboardService.GetDashboard(c.Req.Context(), query) |  | ||||||
| 
 | 
 | ||||||
| 		// Grafana admin users may have starred dashboards in multiple orgs.  This will avoid returning errors when the dashboard is in another org
 | 		uids = make([]string, len(starredDashboards)) | ||||||
| 		if err == nil { | 		for i, dash := range starredDashboards { | ||||||
| 			uids = append(uids, queryResult.UID) | 			uids[i] = dash.UID | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	return response.JSON(200, uids) | 	return response.JSON(200, uids) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue