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}) | ||||
| 	} | ||||
| 
 | ||||
| 	if query.IsStarred { | ||||
| 		filters = append(filters, searchstore.StarredFilter{UserId: query.SignedInUser.UserID}) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(query.Title) > 0 { | ||||
| 		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/sqlstore" | ||||
| 	"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/user" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
|  | @ -35,11 +33,9 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { | |||
| 	var cfg *setting.Cfg | ||||
| 	var savedFolder, savedDash, savedDash2 *dashboards.Dashboard | ||||
| 	var dashboardStore dashboards.Store | ||||
| 	var starService star.Service | ||||
| 
 | ||||
| 	setup := func() { | ||||
| 		sqlStore, cfg = db.InitTestDBwithCfg(t) | ||||
| 		starService = starimpl.ProvideService(sqlStore, cfg) | ||||
| 		quotaService := quotatest.New(false, nil) | ||||
| 		var err error | ||||
| 		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) | ||||
| 	}) | ||||
| 
 | ||||
| 	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) { | ||||
| 		setup() | ||||
| 		// setup() saves one dashboard in the general folder and two in the "savedFolder".
 | ||||
|  |  | |||
|  | @ -405,7 +405,6 @@ type FindPersistedDashboardsQuery struct { | |||
| 	Title         string | ||||
| 	OrgId         int64 | ||||
| 	SignedInUser  *user.SignedInUser | ||||
| 	IsStarred     bool | ||||
| 	DashboardIds  []int64 | ||||
| 	DashboardUIDs []string | ||||
| 	Type          string | ||||
|  |  | |||
|  | @ -224,11 +224,19 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool | |||
| func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) { | ||||
| 	ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator) | ||||
| 	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 { | ||||
| 		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 | ||||
|  |  | |||
|  | @ -345,25 +345,20 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *contextmodel.ReqContext) ([]* | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	starredDashboards := []*dashboards.Dashboard{} | ||||
| 	starredDashboardsCounter := 0 | ||||
| 	for dashboardId := range starredDashboardResult.UserStars { | ||||
| 	if len(starredDashboardResult.UserStars) > 0 { | ||||
| 		var ids []int64 | ||||
| 		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
 | ||||
| 		if starredDashboardsCounter > 50 { | ||||
| 			break | ||||
| 		if len(starredDashboards) > 50 { | ||||
| 			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 { | ||||
| 			return starredDashboards[i].Title < starredDashboards[j].Title | ||||
| 		}) | ||||
|  |  | |||
|  | @ -58,10 +58,30 @@ type SearchService struct { | |||
| } | ||||
| 
 | ||||
| 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{ | ||||
| 		Title:         query.Title, | ||||
| 		SignedInUser:  query.SignedInUser, | ||||
| 		IsStarred:     query.IsStarred, | ||||
| 		DashboardUIDs: query.DashboardUIDs, | ||||
| 		DashboardIds:  query.DashboardIds, | ||||
| 		Type:          query.Type, | ||||
|  | @ -85,11 +105,24 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) error { | |||
| 		hits = sortedHits(hits) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := s.setStarredDashboards(ctx, query.SignedInUser.UserID, hits); err != nil { | ||||
| 		return err | ||||
| 	// set starred dashboards
 | ||||
| 	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 | ||||
| } | ||||
|  | @ -106,22 +139,3 @@ func sortedHits(unsorted model.HitList) model.HitList { | |||
| 
 | ||||
| 	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, "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} | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 	Dialect migrator.Dialect | ||||
| 	Title   string | ||||
|  |  | |||
|  | @ -52,22 +52,26 @@ func (api *API) GetStars(c *contextmodel.ReqContext) response.Response { | |||
| 
 | ||||
| 	iuserstars, err := api.starService.GetByUser(c.Req.Context(), &query) | ||||
| 	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{} | ||||
| 	for dashboardId := range iuserstars.UserStars { | ||||
| 		query := &dashboards.GetDashboardQuery{ | ||||
| 			ID:    dashboardId, | ||||
| 			OrgID: c.OrgID, | ||||
| 	if len(iuserstars.UserStars) > 0 { | ||||
| 		var ids []int64 | ||||
| 		for id := range iuserstars.UserStars { | ||||
| 			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
 | ||||
| 		if err == nil { | ||||
| 			uids = append(uids, queryResult.UID) | ||||
| 		uids = make([]string, len(starredDashboards)) | ||||
| 		for i, dash := range starredDashboards { | ||||
| 			uids[i] = dash.UID | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return response.JSON(200, uids) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue