diff --git a/pkg/infra/db/sqlbuilder.go b/pkg/infra/db/sqlbuilder.go index 8d55f41fa7c..392c5e78a7e 100644 --- a/pkg/infra/db/sqlbuilder.go +++ b/pkg/infra/db/sqlbuilder.go @@ -60,7 +60,7 @@ func (sb *SQLBuilder) AddParams(params ...interface{}) { sb.params = append(sb.params, params...) } -func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission dashboards.PermissionType) { +func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission dashboards.PermissionType, queryType string) { var ( sql string params []interface{} @@ -68,7 +68,7 @@ func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, pe recQryParams []interface{} ) if !ac.IsDisabled(sb.cfg) { - filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, permission, "", sb.features, sb.recursiveQueriesAreSupported) + filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, permission, queryType, sb.features, sb.recursiveQueriesAreSupported) sql, params = filterRBAC.Where() recQry, recQryParams = filterRBAC.With() } else { diff --git a/pkg/infra/db/sqlbuilder_test.go b/pkg/infra/db/sqlbuilder_test.go index 0d1f3e45ba4..2f959480dd3 100644 --- a/pkg/infra/db/sqlbuilder_test.go +++ b/pkg/infra/db/sqlbuilder_test.go @@ -480,7 +480,7 @@ func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, acl var res []*dashboardResponse builder.Write("SELECT * FROM dashboard WHERE true") - builder.WriteDashboardPermissionFilter(signedInUser, search.RequiredPermission) + builder.WriteDashboardPermissionFilter(signedInUser, search.RequiredPermission, "") t.Logf("Searching for dashboards, SQL: %q\n", builder.GetSQLString()) err = sqlStore.GetEngine().SQL(builder.GetSQLString(), builder.params...).Find(&res) require.NoError(t, err) diff --git a/pkg/services/alerting/store.go b/pkg/services/alerting/store.go index a60b1591ffa..af01db644f2 100644 --- a/pkg/services/alerting/store.go +++ b/pkg/services/alerting/store.go @@ -167,7 +167,7 @@ func (ss *sqlStore) HandleAlertsQuery(ctx context.Context, query *alertmodels.Ge builder.Write(")") } - builder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW) + builder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW, "") builder.Write(" ORDER BY name ASC") diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 5c1613a1775..bf476134c67 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -250,7 +250,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed builder.Write(getFromLibraryElementDTOWithMeta(store.GetDialect())) builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0") writeParamSelectorSQL(&builder, params...) - builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW) + builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "") builder.Write(` OR dashboard.id=0`) if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElements); err != nil { return err @@ -372,7 +372,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI return err } if signedInUser.OrgRole != org.RoleAdmin { - builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW) + builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "") } if query.SortDirection == search.SortAlphaDesc.Name { builder.Write(" ORDER BY 1 DESC") @@ -603,7 +603,7 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser * builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id") builder.Write(` WHERE lec.element_id=?`, element.ID) if signedInUser.OrgRole != org.RoleAdmin { - builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW) + builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "") } if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElementConnections); err != nil { return err diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index 31150e7eb91..05daebc2f97 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -90,7 +90,24 @@ func (api *Api) RegisterAPIEndpoints() { // ListPublicDashboards Gets list of public dashboards by orgId // GET /api/dashboards/public-dashboards func (api *Api) ListPublicDashboards(c *contextmodel.ReqContext) response.Response { - resp, err := api.PublicDashboardService.FindAll(c.Req.Context(), c.SignedInUser, c.OrgID) + perPage := c.QueryInt("perpage") + if perPage <= 0 { + perPage = 1000 + } + + page := c.QueryInt("page") + if page < 1 { + page = 1 + } + + resp, err := api.PublicDashboardService.FindAllWithPagination(c.Req.Context(), &PublicDashboardListQuery{ + OrgID: c.OrgID, + Query: c.Query("query"), + Page: page, + Limit: perPage, + User: c.SignedInUser, + }) + if err != nil { return response.Err(err) } diff --git a/pkg/services/publicdashboards/api/api_test.go b/pkg/services/publicdashboards/api/api_test.go index a4713c9eb5c..ab0c7ce9dbf 100644 --- a/pkg/services/publicdashboards/api/api_test.go +++ b/pkg/services/publicdashboards/api/api_test.go @@ -89,19 +89,21 @@ func TestAPIFeatureFlag(t *testing.T) { } func TestAPIListPublicDashboard(t *testing.T) { - successResp := []PublicDashboardListResponse{ - { - Uid: "1234asdfasdf", - AccessToken: "asdfasdf", - DashboardUid: "abc1234", - IsEnabled: true, + successResp := &PublicDashboardListResponseWithPagination{ + PublicDashboards: []*PublicDashboardListResponse{ + { + Uid: "1234asdfasdf", + AccessToken: "asdfasdf", + DashboardUid: "abc1234", + IsEnabled: true, + }, }, } testCases := []struct { Name string User *user.SignedInUser - Response []PublicDashboardListResponse + Response *PublicDashboardListResponseWithPagination ResponseErr error ExpectedHttpResponse int }{ @@ -131,7 +133,7 @@ func TestAPIListPublicDashboard(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { service := publicdashboards.NewFakePublicDashboardService(t) - service.On("FindAll", mock.Anything, mock.Anything, mock.Anything). + service.On("FindAllWithPagination", mock.Anything, mock.Anything, mock.Anything). Return(test.Response, test.ResponseErr).Maybe() cfg := setting.NewCfg() @@ -143,10 +145,10 @@ func TestAPIListPublicDashboard(t *testing.T) { assert.Equal(t, test.ExpectedHttpResponse, response.Code) if test.ExpectedHttpResponse == http.StatusOK { - var jsonResp []PublicDashboardListResponse + var jsonResp PublicDashboardListResponseWithPagination err := json.Unmarshal(response.Body.Bytes(), &jsonResp) require.NoError(t, err) - assert.Equal(t, jsonResp[0].Uid, "1234asdfasdf") + assert.Equal(t, jsonResp.PublicDashboards[0].Uid, "1234asdfasdf") } if test.ResponseErr != nil { @@ -155,7 +157,7 @@ func TestAPIListPublicDashboard(t *testing.T) { require.NoError(t, err) assert.Equal(t, "Internal server error", errResp.Message) assert.Equal(t, "publicdashboards.internalServerError", errResp.MessageID) - service.AssertNotCalled(t, "FindAll") + service.AssertNotCalled(t, "FindAllWithPagination") } }) } diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 5a04831607d..ca8c79c4656 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -322,7 +322,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) annotationsService := annotationstest.NewFakeAnnotationsRepo() // create public dashboard - store := publicdashboardsStore.ProvideStore(db) + store := publicdashboardsStore.ProvideStore(db, db.Cfg, featuremgmt.WithFeatures()) cfg := setting.NewCfg() ac := acmock.New() ws := publicdashboardsService.ProvideServiceWrapper(store) diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index 6882ef2058e..d4a214d7364 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -7,9 +7,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" + "github.com/grafana/grafana/pkg/setting" ) // Define the storage implementation. We're generating the mock implementation @@ -17,6 +21,8 @@ import ( type PublicDashboardStoreImpl struct { sqlStore db.DB log log.Logger + cfg *setting.Cfg + features featuremgmt.FeatureToggles } var LogPrefix = "publicdashboards.store" @@ -26,25 +32,54 @@ var LogPrefix = "publicdashboards.store" var _ publicdashboards.Store = (*PublicDashboardStoreImpl)(nil) // Factory used by wire to dependency injection -func ProvideStore(sqlStore db.DB) *PublicDashboardStoreImpl { +func ProvideStore(sqlStore db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles) *PublicDashboardStoreImpl { return &PublicDashboardStoreImpl{ sqlStore: sqlStore, log: log.New(LogPrefix), + cfg: cfg, + features: features, } } -// FindAll Returns a list of public dashboards by orgId -func (d *PublicDashboardStoreImpl) FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) { - resp := make([]PublicDashboardListResponse, 0) +// FindAllWithPagination Returns a list of public dashboards by orgId, based on permissions and with pagination +func (d *PublicDashboardStoreImpl) FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error) { + resp := &PublicDashboardListResponseWithPagination{ + PublicDashboards: make([]*PublicDashboardListResponse, 0), + TotalCount: 0, + } - err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { - sess.Table("dashboard_public").Select( - "dashboard_public.uid, dashboard_public.access_token, dashboard.uid as dashboard_uid, dashboard_public.is_enabled, dashboard.title"). - Join("LEFT", "dashboard", "dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id"). - Where("dashboard_public.org_id = ?", orgId). - OrderBy(" dashboard.title IS NULL, dashboard.title ASC") + recursiveQueriesAreSupported, err := d.sqlStore.RecursiveQueriesAreSupported() + if err != nil { + return nil, err + } - err := sess.Find(&resp) + pubdashBuilder := db.NewSqlBuilder(d.cfg, d.features, d.sqlStore.GetDialect(), recursiveQueriesAreSupported) + pubdashBuilder.Write("SELECT dashboard_public.uid, dashboard_public.access_token, dashboard.uid as dashboard_uid, dashboard_public.is_enabled, dashboard.title") + pubdashBuilder.Write(" FROM dashboard_public") + pubdashBuilder.Write(" JOIN dashboard ON dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id") + pubdashBuilder.Write(` WHERE dashboard_public.org_id = ?`, query.OrgID) + if query.User.OrgRole != org.RoleAdmin { + pubdashBuilder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard) + } + pubdashBuilder.Write(" ORDER BY dashboard.title") + pubdashBuilder.Write(d.sqlStore.GetDialect().LimitOffset(int64(query.Limit), int64(query.Offset))) + + counterBuilder := db.NewSqlBuilder(d.cfg, d.features, d.sqlStore.GetDialect(), recursiveQueriesAreSupported) + counterBuilder.Write("SELECT COUNT(*)") + counterBuilder.Write(" FROM dashboard_public") + counterBuilder.Write(" JOIN dashboard ON dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id") + counterBuilder.Write(` WHERE dashboard_public.org_id = ?`, query.OrgID) + if query.User.OrgRole != org.RoleAdmin { + counterBuilder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard) + } + + err = d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + err := sess.SQL(pubdashBuilder.GetSQLString(), pubdashBuilder.GetParams()...).Find(&resp.PublicDashboards) + if err != nil { + return err + } + + _, err = sess.SQL(counterBuilder.GetSQLString(), counterBuilder.GetParams()...).Get(&resp.TotalCount) return err }) diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 9c66558e85f..eb3edaa469b 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -2,18 +2,24 @@ package database import ( "context" + "fmt" + + "strings" "testing" "time" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/service" "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/assert" @@ -34,38 +40,122 @@ func TestIntegrationListPublicDashboard(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - sqlStore, cfg := db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) - quotaService := quotatest.New(false, nil) - dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) - require.NoError(t, err) - publicdashboardStore := ProvideStore(sqlStore) + + var sqlStore *sqlstore.SQLStore + var cfg *setting.Cfg + + var aDash *dashboards.Dashboard + var bDash *dashboards.Dashboard + var cDash *dashboards.Dashboard + + var aPublicDash *PublicDashboard + var bPublicDash *PublicDashboard + var cPublicDash *PublicDashboard var orgId int64 = 1 - bDash := insertTestDashboard(t, dashboardStore, "b", orgId, 0, true) - aDash := insertTestDashboard(t, dashboardStore, "a", orgId, 0, true) - cDash := insertTestDashboard(t, dashboardStore, "c", orgId, 0, true) + var publicdashboardStore *PublicDashboardStoreImpl - // these are in order of how they should be returned from ListPUblicDashboards - a := insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType) - b := insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true, PublicShareType) - c := insertPublicDashboard(t, publicdashboardStore, cDash.UID, orgId, true, PublicShareType) + setup := func() { + sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) + quotaService := quotatest.New(false, nil) + dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) + require.NoError(t, err) + publicdashboardStore = ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - // this is case that can happen as of now, however, postgres and mysql sort - // null in the exact opposite fashion and there is no shared syntax to sort - // nulls in the same way in all 3 db's. - //d := insertPublicDashboard(t, publicdashboardStore, "missing", orgId, false) + bDash = insertTestDashboard(t, dashboardStore, "b", orgId, 0, false) + aDash = insertTestDashboard(t, dashboardStore, "a", orgId, 0, false) + cDash = insertTestDashboard(t, dashboardStore, "c", orgId, 0, false) - // should not be included in response - _ = insertPublicDashboard(t, publicdashboardStore, "wrongOrgId", 777, false, PublicShareType) + // these are in order of how they should be returned from ListPUblicDashboards + aPublicDash = insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType) + bPublicDash = insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true, PublicShareType) + cPublicDash = insertPublicDashboard(t, publicdashboardStore, cDash.UID, orgId, true, PublicShareType) + } - resp, err := publicdashboardStore.FindAll(context.Background(), orgId) - require.NoError(t, err) + t.Run("FindAllWithPagination will return dashboard list based on orgId with pagination", func(t *testing.T) { + setup() - assert.Len(t, resp, 3) - assert.Equal(t, resp[0].Uid, a.Uid) - assert.Equal(t, resp[1].Uid, b.Uid) - assert.Equal(t, resp[2].Uid, c.Uid) + // should not be included in response + _ = insertPublicDashboard(t, publicdashboardStore, "wrongOrgId", 777, false, PublicShareType) + + permissions := []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", aDash.UID)}, + {Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", bDash.UID)}, + {Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", cDash.UID)}, + } + + err := insertPermissions(sqlStore, orgId, "viewer", permissions) + require.NoError(t, err) + + query := &PublicDashboardListQuery{ + User: &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}}, + OrgID: orgId, + Page: 1, + Limit: 50, + Offset: 0, + } + resp, err := publicdashboardStore.FindAllWithPagination(context.Background(), query) + require.NoError(t, err) + + assert.Len(t, resp.PublicDashboards, 3) + assert.Equal(t, resp.PublicDashboards[0].Uid, aPublicDash.Uid) + assert.Equal(t, resp.PublicDashboards[1].Uid, bPublicDash.Uid) + assert.Equal(t, resp.PublicDashboards[2].Uid, cPublicDash.Uid) + assert.Equal(t, resp.TotalCount, int64(3)) + }) + + t.Run("FindAllWithPagination will return dashboard list based on read permissions with pagination", func(t *testing.T) { + setup() + + permissions := []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", aDash.UID)}, + {Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", cDash.UID)}, + } + + err := insertPermissions(sqlStore, orgId, "viewer", permissions) + require.NoError(t, err) + + query := &PublicDashboardListQuery{ + User: &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}}, + OrgID: orgId, + Page: 1, + Limit: 50, + Offset: 0, + } + resp, err := publicdashboardStore.FindAllWithPagination(context.Background(), query) + require.NoError(t, err) + + assert.Len(t, resp.PublicDashboards, 2) + assert.Equal(t, resp.PublicDashboards[0].Uid, aPublicDash.Uid) + assert.Equal(t, resp.PublicDashboards[1].Uid, cPublicDash.Uid) + assert.Equal(t, resp.TotalCount, int64(2)) + }) + + t.Run("FindAllWithPagination will return empty dashboard list based on read permissions with pagination", func(t *testing.T) { + setup() + + permissions := []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:another-dashboard-uid"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:another-dashboard-2-uid"}, + } + + err := insertPermissions(sqlStore, orgId, "viewer", permissions) + require.NoError(t, err) + + query := &PublicDashboardListQuery{ + User: &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}}, + OrgID: orgId, + Page: 1, + Limit: 50, + Offset: 0, + } + resp, err := publicdashboardStore.FindAllWithPagination(context.Background(), query) + require.NoError(t, err) + + assert.Len(t, resp.PublicDashboards, 0) + assert.Equal(t, resp.TotalCount, int64(0)) + }) } func TestIntegrationFindDashboard(t *testing.T) { @@ -84,7 +174,7 @@ func TestIntegrationFindDashboard(t *testing.T) { store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) dashboardStore = store - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } @@ -114,7 +204,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) dashboardStore = store - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } t.Run("ExistsEnabledByAccessToken will return true when at least one public dashboard has a matching access token", func(t *testing.T) { @@ -187,7 +277,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) dashboardStore = store - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } @@ -252,7 +342,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) dashboardStore = store - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } @@ -319,7 +409,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) { sqlStore, cfg = db.InitTestDBwithCfg(t) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil)) require.NoError(t, err) - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } @@ -389,7 +479,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) dashboardStore = store - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true) insertPublicDashboard(t, publicdashboardStore, savedDashboard2.UID, savedDashboard2.OrgID, false, PublicShareType) @@ -468,7 +558,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) anotherSavedDashboard = insertTestDashboard(t, dashboardStore, "test another Dashie", 1, 0, true) } @@ -572,7 +662,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } t.Run("GetOrgIdByAccessToken will OrgId when enabled", func(t *testing.T) { @@ -644,7 +734,7 @@ func TestIntegrationDelete(t *testing.T) { sqlStore, cfg = db.InitTestDBwithCfg(t) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil)) require.NoError(t, err) - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, true, PublicShareType) } @@ -675,7 +765,7 @@ func TestGetDashboardByFolder(t *testing.T) { t.Run("returns nil when dashboard is not a folder", func(t *testing.T) { sqlStore, _ := db.InitTestDBwithCfg(t) dashboard := &dashboards.Dashboard{IsFolder: false} - store := ProvideStore(sqlStore) + store := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) pubdashes, err := store.FindByDashboardFolder(context.Background(), dashboard) require.NoError(t, err) @@ -684,7 +774,7 @@ func TestGetDashboardByFolder(t *testing.T) { t.Run("returns nil when dashboard is nil", func(t *testing.T) { sqlStore, _ := db.InitTestDBwithCfg(t) - store := ProvideStore(sqlStore) + store := ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) pubdashes, err := store.FindByDashboardFolder(context.Background(), nil) require.NoError(t, err) @@ -696,7 +786,7 @@ func TestGetDashboardByFolder(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - pubdashStore := ProvideStore(sqlStore) + pubdashStore := ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "title", 1, 1, true, PublicShareType) pubdash := insertPublicDashboard(t, pubdashStore, dashboard.UID, dashboard.OrgID, true, PublicShareType) dashboard2 := insertTestDashboard(t, dashboardStore, "title", 1, 2, true, PublicShareType) @@ -729,7 +819,7 @@ func TestGetMetrics(t *testing.T) { store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) dashboardStore = store - publicdashboardStore = ProvideStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore, cfg, featuremgmt.WithFeatures()) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, false) savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, false) savedDashboard3 = insertTestDashboard(t, dashboardStore, "testDashie3", 2, 0, false) @@ -827,3 +917,50 @@ func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardSt return pubdash } + +func insertPermissions(sqlStore *sqlstore.SQLStore, orgId int64, role string, permissions []accesscontrol.Permission) error { + return sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + newRole := &accesscontrol.Role{ + OrgID: orgId, + UID: fmt.Sprintf("basic_%s", role), + Name: fmt.Sprintf("basic:%s", role), + Updated: time.Now(), + Created: time.Now(), + } + _, err := sess.Insert(newRole) + if err != nil { + return err + } + + _, err = sess.Insert(accesscontrol.BuiltinRole{ + OrgID: orgId, + RoleID: newRole.ID, + Role: strings.ToUpper(role[:1]) + role[1:], + Created: time.Now(), + Updated: time.Now(), + }) + if err != nil { + return err + } + + for i := range permissions { + permissions[i].RoleID = newRole.ID + permissions[i].Created = time.Now() + permissions[i].Updated = time.Now() + } + + _, err = sess.InsertMulti(&permissions) + if err != nil { + return err + } + + _, err = sess.Insert(accesscontrol.UserRole{ + OrgID: orgId, + RoleID: newRole.ID, + UserID: 1, + Created: time.Now(), + }) + + return err + }) +} diff --git a/pkg/services/publicdashboards/models/models.go b/pkg/services/publicdashboards/models/models.go index 31f6d725af5..254a3819c1c 100644 --- a/pkg/services/publicdashboards/models/models.go +++ b/pkg/services/publicdashboards/models/models.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana/pkg/kinds/dashboard" + "github.com/grafana/grafana/pkg/services/user" ) // PublicDashboardErr represents a dashboard error. @@ -103,6 +104,22 @@ func (pd PublicDashboard) TableName() string { return "dashboard_public" } +type PublicDashboardListQuery struct { + OrgID int64 + Query string + Page int + Limit int + Offset int + User *user.SignedInUser +} + +type PublicDashboardListResponseWithPagination struct { + PublicDashboards []*PublicDashboardListResponse `json:"publicDashboards"` + TotalCount int64 `json:"totalCount"` + Page int `json:"page"` + PerPage int `json:"perPage"` +} + type PublicDashboardListResponse struct { Uid string `json:"uid" xorm:"uid"` AccessToken string `json:"accessToken" xorm:"access_token"` diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index 06560513433..4bc9691d548 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -139,22 +139,22 @@ func (_m *FakePublicDashboardService) Find(ctx context.Context, uid string) (*mo return r0, r1 } -// FindAll provides a mock function with given fields: ctx, u, orgId -func (_m *FakePublicDashboardService) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]models.PublicDashboardListResponse, error) { - ret := _m.Called(ctx, u, orgId) +// FindAllWithPagination provides a mock function with given fields: ctx, query +func (_m *FakePublicDashboardService) FindAllWithPagination(ctx context.Context, query *models.PublicDashboardListQuery) (*models.PublicDashboardListResponseWithPagination, error) { + ret := _m.Called(ctx, query) - var r0 []models.PublicDashboardListResponse - if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64) []models.PublicDashboardListResponse); ok { - r0 = rf(ctx, u, orgId) + var r0 *models.PublicDashboardListResponseWithPagination + if rf, ok := ret.Get(0).(func(context.Context, *models.PublicDashboardListQuery) *models.PublicDashboardListResponseWithPagination); ok { + r0 = rf(ctx, query) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.PublicDashboardListResponse) + r0 = ret.Get(0).(*models.PublicDashboardListResponseWithPagination) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64) error); ok { - r1 = rf(ctx, u, orgId) + if rf, ok := ret.Get(1).(func(context.Context, *models.PublicDashboardListQuery) error); ok { + r1 = rf(ctx, query) } else { r1 = ret.Error(1) } diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index 7a0b583e231..4f0fdc4cd9f 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -123,22 +123,22 @@ func (_m *FakePublicDashboardStore) Find(ctx context.Context, uid string) (*mode return r0, r1 } -// FindAll provides a mock function with given fields: ctx, orgId -func (_m *FakePublicDashboardStore) FindAll(ctx context.Context, orgId int64) ([]models.PublicDashboardListResponse, error) { - ret := _m.Called(ctx, orgId) +// FindAllWithPagination provides a mock function with given fields: ctx, query +func (_m *FakePublicDashboardStore) FindAllWithPagination(ctx context.Context, query *models.PublicDashboardListQuery) (*models.PublicDashboardListResponseWithPagination, error) { + ret := _m.Called(ctx, query) - var r0 []models.PublicDashboardListResponse - if rf, ok := ret.Get(0).(func(context.Context, int64) []models.PublicDashboardListResponse); ok { - r0 = rf(ctx, orgId) + var r0 *models.PublicDashboardListResponseWithPagination + if rf, ok := ret.Get(0).(func(context.Context, *models.PublicDashboardListQuery) *models.PublicDashboardListResponseWithPagination); ok { + r0 = rf(ctx, query) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.PublicDashboardListResponse) + r0 = ret.Get(0).(*models.PublicDashboardListResponseWithPagination) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { - r1 = rf(ctx, orgId) + if rf, ok := ret.Get(1).(func(context.Context, *models.PublicDashboardListQuery) error); ok { + r1 = rf(ctx, query) } else { r1 = ret.Error(1) } diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index def7c718eb6..fc1e98ed2ad 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -21,7 +21,7 @@ type Service interface { FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) - FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) + FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error) Find(ctx context.Context, uid string) (*PublicDashboard, error) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) @@ -52,7 +52,7 @@ type Store interface { FindByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, error) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) - FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) + FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error) Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) Delete(ctx context.Context, uid string) (int64, error) diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index 68f55627f15..61c9296e43c 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -663,7 +663,7 @@ func TestGetQueryDataResponse(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) fakeQueryService := &query.FakeQueryService{} fakeQueryService.On("QueryData", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&backend.QueryDataResponse{}, nil) @@ -1109,7 +1109,7 @@ func TestGetMetricRequest(t *testing.T) { sqlStore := db.InitTestDB(t) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) publicDashboard := &PublicDashboard{ Uid: "1", @@ -1194,7 +1194,7 @@ func TestBuildMetricRequest(t *testing.T) { sqlStore := db.InitTestDB(t) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil)) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true, []map[string]interface{}{}, nil) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 1e823b4c056..d1e22173d5c 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -276,14 +276,18 @@ func (pd *PublicDashboardServiceImpl) NewPublicDashboardAccessToken(ctx context. return "", ErrInternalServerError.Errorf("failed to generate a unique accessToken for public dashboard") } -// FindAll Returns a list of public dashboards by orgId -func (pd *PublicDashboardServiceImpl) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) { - publicDashboards, err := pd.store.FindAll(ctx, orgId) +// FindAllWithPagination Returns a list of public dashboards by orgId, based on permissions and with pagination +func (pd *PublicDashboardServiceImpl) FindAllWithPagination(ctx context.Context, query *PublicDashboardListQuery) (*PublicDashboardListResponseWithPagination, error) { + query.Offset = query.Limit * (query.Page - 1) + resp, err := pd.store.FindAllWithPagination(ctx, query) if err != nil { - return nil, ErrInternalServerError.Errorf("FindAll: %w", err) + return nil, ErrInternalServerError.Errorf("FindAllWithPagination: %w", err) } - return pd.filterDashboardsByPermissions(ctx, u, publicDashboards) + resp.Page = query.Page + resp.PerPage = query.Limit + + return resp, nil } func (pd *PublicDashboardServiceImpl) ExistsEnabledByDashboardUid(ctx context.Context, dashboardUid string) (bool, error) { @@ -371,25 +375,6 @@ func (pd *PublicDashboardServiceImpl) logIsEnabledChanged(existingPubdash *Publi } } -// Filter out dashboards that user does not have read access to -func (pd *PublicDashboardServiceImpl) filterDashboardsByPermissions(ctx context.Context, u *user.SignedInUser, publicDashboards []PublicDashboardListResponse) ([]PublicDashboardListResponse, error) { - result := make([]PublicDashboardListResponse, 0) - - for i := range publicDashboards { - hasAccess, err := pd.ac.Evaluate(ctx, u, accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(publicDashboards[i].DashboardUid))) - // If original dashboard does not exist, the public dashboard is an orphan. We want to list it anyway - if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return nil, ErrInternalServerError.Errorf("filterDashboardsByPermissions: error evaluating permissions %w", err) - } - - // If user has access to the original dashboard or the dashboard does not exist, add the pubdash to the result - if hasAccess || errors.Is(err, dashboards.ErrDashboardNotFound) { - result = append(result, publicDashboards[i]) - } - } - return result, nil -} - // Checks to see if PublicDashboard.ExistsEnabledByDashboardUid is true on create or changed on update func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdash *PublicDashboard) bool { // creating dashboard, enabled true diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index bd92a6d3f12..7cebebcd5ba 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -195,7 +195,7 @@ func TestCreatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) @@ -285,7 +285,7 @@ func TestCreatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) @@ -325,7 +325,7 @@ func TestCreatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) @@ -359,7 +359,7 @@ func TestCreatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) templateVars := make([]map[string]interface{}, 1) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, templateVars, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) @@ -474,7 +474,7 @@ func TestCreatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) @@ -519,7 +519,7 @@ func TestUpdatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) @@ -588,7 +588,7 @@ func TestUpdatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) @@ -686,7 +686,7 @@ func TestUpdatePublicDashboard(t *testing.T) { quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) serviceWrapper := ProvideServiceWrapper(publicdashboardStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) @@ -915,152 +915,15 @@ func TestDashboardEnabledChanged(t *testing.T) { func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { type args struct { ctx context.Context - u *user.SignedInUser - orgId int64 + query *PublicDashboardListQuery } - testCases := []struct { - name string - args args - evaluateFunc func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error) - want []PublicDashboardListResponse - wantErr assert.ErrorAssertionFunc - }{ - { - name: "should return empty list when user does not have permissions to read any dashboard", - args: args{ - ctx: context.Background(), - u: &user.SignedInUser{OrgID: 1}, - orgId: 1, - }, - want: []PublicDashboardListResponse{}, - wantErr: assert.NoError, - }, - { - name: "should return all dashboards when has permissions", - args: args{ - ctx: context.Background(), - u: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ - 1: {"dashboards:read": { - "dashboards:uid:0S6TmO67z", "dashboards:uid:1S6TmO67z", "dashboards:uid:2S6TmO67z", "dashboards:uid:9S6TmO67z", - }}}, - }, - orgId: 1, - }, - want: []PublicDashboardListResponse{ - { - Uid: "0GwW7mgVk", - AccessToken: "0b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "0S6TmO67z", - Title: "my zero dashboard", - IsEnabled: true, - }, - { - Uid: "1GwW7mgVk", - AccessToken: "1b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "1S6TmO67z", - Title: "my first dashboard", - IsEnabled: true, - }, - { - Uid: "2GwW7mgVk", - AccessToken: "2b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "2S6TmO67z", - Title: "my second dashboard", - IsEnabled: false, - }, - { - Uid: "9GwW7mgVk", - AccessToken: "deletedashboardaccesstoken", - DashboardUid: "9S6TmO67z", - Title: "", - IsEnabled: true, - }, - }, - wantErr: assert.NoError, - }, - { - name: "should return only dashboards with permissions", - args: args{ - ctx: context.Background(), - u: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ - 1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}}, - }, - orgId: 1, - }, - want: []PublicDashboardListResponse{ - { - Uid: "0GwW7mgVk", - AccessToken: "0b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "0S6TmO67z", - Title: "my zero dashboard", - IsEnabled: true, - }, - }, - wantErr: assert.NoError, - }, - { - name: "should return orphaned public dashboards", - args: args{ - ctx: context.Background(), - u: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ - 1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}}, - }, - orgId: 1, - }, - evaluateFunc: func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error) { - return false, dashboards.ErrDashboardNotFound - }, - want: []PublicDashboardListResponse{ - { - Uid: "0GwW7mgVk", - AccessToken: "0b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "0S6TmO67z", - Title: "my zero dashboard", - IsEnabled: true, - }, - { - Uid: "1GwW7mgVk", - AccessToken: "1b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "1S6TmO67z", - Title: "my first dashboard", - IsEnabled: true, - }, - { - Uid: "2GwW7mgVk", - AccessToken: "2b458cb7fe7f42c68712078bcacee6e3", - DashboardUid: "2S6TmO67z", - Title: "my second dashboard", - IsEnabled: false, - }, - { - Uid: "9GwW7mgVk", - AccessToken: "deletedashboardaccesstoken", - DashboardUid: "9S6TmO67z", - Title: "", - IsEnabled: true, - }, - }, - wantErr: assert.NoError, - }, - { - name: "errors different than not data found should be returned", - args: args{ - ctx: context.Background(), - u: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ - 1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}}, - }, - orgId: 1, - }, - evaluateFunc: func(c context.Context, u *user.SignedInUser, e accesscontrol.Evaluator) (bool, error) { - return false, dashboards.ErrDashboardCorrupt - }, - want: nil, - wantErr: assert.Error, - }, + type mockResponse struct { + PublicDashboardListResponseWithPagination *PublicDashboardListResponseWithPagination + Err error } - mockedDashboards := []PublicDashboardListResponse{ + mockedDashboards := []*PublicDashboardListResponse{ { Uid: "0GwW7mgVk", AccessToken: "0b458cb7fe7f42c68712078bcacee6e3", @@ -1091,9 +954,62 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { }, } - store := NewFakePublicDashboardStore(t) - store.On("FindAll", mock.Anything, mock.Anything). - Return(mockedDashboards, nil) + testCases := []struct { + name string + args args + want *PublicDashboardListResponseWithPagination + mockResponse *mockResponse + wantErr assert.ErrorAssertionFunc + }{ + { + name: "should return correct pagination response", + args: args{ + ctx: context.Background(), + query: &PublicDashboardListQuery{ + User: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ + 1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}}, + }, + OrgID: 1, + Page: 1, + Limit: 50, + }, + }, + mockResponse: &mockResponse{ + PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ + TotalCount: int64(len(mockedDashboards)), + PublicDashboards: mockedDashboards, + }, + Err: nil, + }, + want: &PublicDashboardListResponseWithPagination{ + Page: 1, + PerPage: 50, + TotalCount: int64(len(mockedDashboards)), + PublicDashboards: mockedDashboards, + }, + wantErr: assert.NoError, + }, + { + name: "should return error when store returns error", + args: args{ + ctx: context.Background(), + query: &PublicDashboardListQuery{ + User: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ + 1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}}, + }, + OrgID: 1, + Page: 1, + Limit: 50, + }, + }, + mockResponse: &mockResponse{ + PublicDashboardListResponseWithPagination: nil, + Err: errors.New("an err"), + }, + want: nil, + wantErr: assert.Error, + }, + } ac := tests.SetupMockAccesscontrol(t, func(c context.Context, siu *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) { @@ -1102,21 +1018,23 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { false, ) - pd := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: store, - ac: ac, - } - for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - ac.EvaluateFunc = tt.evaluateFunc + store := NewFakePublicDashboardStore(t) + store.On("FindAllWithPagination", mock.Anything, mock.Anything). + Return(tt.mockResponse.PublicDashboardListResponseWithPagination, tt.mockResponse.Err) - got, err := pd.FindAll(tt.args.ctx, tt.args.u, tt.args.orgId) - if !tt.wantErr(t, err, fmt.Sprintf("FindAll(%v, %v, %v)", tt.args.ctx, tt.args.u, tt.args.orgId)) { + pd := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: store, + ac: ac, + } + + got, err := pd.FindAllWithPagination(tt.args.ctx, tt.args.query) + if !tt.wantErr(t, err, fmt.Sprintf("FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query)) { return } - assert.Equalf(t, tt.want, got, "FindAll(%v, %v, %v)", tt.args.ctx, tt.args.u, tt.args.orgId) + assert.Equalf(t, tt.want, got, "FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query) }) } } diff --git a/public/app/features/dashboard/api/publicDashboardApi.ts b/public/app/features/dashboard/api/publicDashboardApi.ts index df98071d404..c1d31e70c81 100644 --- a/public/app/features/dashboard/api/publicDashboardApi.ts +++ b/public/app/features/dashboard/api/publicDashboardApi.ts @@ -11,7 +11,10 @@ import { SessionUser, } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardModel } from 'app/features/dashboard/state'; -import { ListPublicDashboardResponse } from 'app/features/manage-dashboards/types'; +import { + PublicDashboardListWithPagination, + PublicDashboardListWithPaginationResponse, +} from 'app/features/manage-dashboards/types'; type ReqOptions = { manageError?: (err: unknown) => { error: unknown }; @@ -137,9 +140,13 @@ export const publicDashboardApi = createApi({ }), providesTags: (result, _, email) => [{ type: 'ActiveUserDashboards', id: email }], }), - listPublicDashboards: builder.query({ - query: () => ({ - url: '/dashboards/public-dashboards', + listPublicDashboards: builder.query({ + query: (page = 1) => ({ + url: `/dashboards/public-dashboards?page=${page}&perpage=8`, + }), + transformResponse: (response: PublicDashboardListWithPaginationResponse) => ({ + ...response, + totalPages: Math.ceil(response.totalCount / response.perPage), }), providesTags: ['AuditTablePublicDashboard'], }), diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx index 2b4d3e6071b..8506a4b8753 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx @@ -11,11 +11,11 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; -import { ListPublicDashboardResponse } from '../../types'; +import { PublicDashboardListResponse, PublicDashboardListWithPaginationResponse } from '../../types'; import { PublicDashboardListTable } from './PublicDashboardListTable'; -const publicDashboardListResponse: ListPublicDashboardResponse[] = [ +const publicDashboardListResponse: PublicDashboardListResponse[] = [ { uid: 'SdZwuCZVz', accessToken: 'beeaf92f6ab3467f80b2be922c7741ab', @@ -32,7 +32,7 @@ const publicDashboardListResponse: ListPublicDashboardResponse[] = [ }, ]; -const orphanedDashboardListResponse: ListPublicDashboardResponse[] = [ +const orphanedDashboardListResponse: PublicDashboardListResponse[] = [ { uid: 'SdZwuCZVz2', accessToken: 'beeaf92f6ab3467f80b2be922c7741ab', @@ -49,9 +49,15 @@ const orphanedDashboardListResponse: ListPublicDashboardResponse[] = [ }, ]; +const paginationResponse: Omit = { + page: 1, + perPage: 50, + totalCount: 50, +}; + const server = setupServer( rest.get('/api/dashboards/public-dashboards', (_, res, ctx) => - res(ctx.status(200), ctx.json(publicDashboardListResponse)) + res(ctx.status(200), ctx.json({ ...paginationResponse, publicDashboards: publicDashboardListResponse })) ), rest.delete('/api/dashboards/uid/:dashboardUid/public-dashboards/:uid', (_, res, ctx) => res(ctx.status(200))) ); @@ -104,9 +110,16 @@ describe('Show table', () => { expect(screen.getAllByRole('listitem')).toHaveLength(publicDashboardListResponse.length); }); it('renders empty list', async () => { + const emptyListRS: PublicDashboardListWithPaginationResponse = { + publicDashboards: [], + totalCount: 0, + page: 1, + perPage: 50, + }; + server.use( rest.get('/api/dashboards/public-dashboards', (req, res, ctx) => { - return res(ctx.status(200), ctx.json([])); + return res(ctx.status(200), ctx.json(emptyListRS)); }) ); @@ -149,7 +162,10 @@ describe('Delete public dashboard', () => { describe('Orphaned public dashboard', () => { it('renders orphaned and non orphaned public dashboards items correctly', async () => { - const response = [...publicDashboardListResponse, ...orphanedDashboardListResponse]; + const response: PublicDashboardListWithPaginationResponse = { + ...paginationResponse, + publicDashboards: [...publicDashboardListResponse, ...orphanedDashboardListResponse], + }; server.use( rest.get('/api/dashboards/public-dashboards', (req, res, ctx) => { return res(ctx.status(200), ctx.json(response)); @@ -158,13 +174,13 @@ describe('Orphaned public dashboard', () => { jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); await renderPublicDashboardTable(true); - response.forEach((pd, idx) => { + response.publicDashboards.forEach((pd, idx) => { renderPublicDashboardItemCorrectly(pd, idx, true); }); }); }); -const renderPublicDashboardItemCorrectly = (pd: ListPublicDashboardResponse, idx: number, hasWriteAccess: boolean) => { +const renderPublicDashboardItemCorrectly = (pd: PublicDashboardListResponse, idx: number, hasWriteAccess: boolean) => { const isOrphaned = !pd.dashboardUid; const cardItems = screen.getAllByRole('listitem'); diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx index f8ffb6d2191..eb9184b868a 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx @@ -1,11 +1,22 @@ import { css } from '@emotion/css'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { useMedia } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data/src'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { reportInteraction } from '@grafana/runtime'; -import { LinkButton, useStyles2, Spinner, Card, useTheme2, Tooltip, Icon, Switch } from '@grafana/ui/src'; +import { + LinkButton, + useStyles2, + Spinner, + Card, + useTheme2, + Tooltip, + Icon, + Switch, + Pagination, + HorizontalGroup, +} from '@grafana/ui/src'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/services/context_srv'; import { @@ -19,11 +30,11 @@ import { import { isOrgAdmin } from 'app/features/plugins/admin/permissions'; import { AccessControlAction } from 'app/types'; -import { ListPublicDashboardResponse } from '../../types'; +import { PublicDashboardListResponse } from '../../types'; import { DeletePublicDashboardButton } from './DeletePublicDashboardButton'; -const PublicDashboardCard = ({ pd }: { pd: ListPublicDashboardResponse }) => { +const PublicDashboardCard = ({ pd }: { pd: PublicDashboardListResponse }) => { const styles = useStyles2(getStyles); const theme = useTheme2(); const isMobile = useMedia(`(max-width: ${theme.breakpoints.values.sm}px)`); @@ -34,7 +45,7 @@ const PublicDashboardCard = ({ pd }: { pd: ListPublicDashboardResponse }) => { const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin()); const isOrphaned = !pd.dashboardUid; - const onTogglePause = (pd: ListPublicDashboardResponse, isPaused: boolean) => { + const onTogglePause = (pd: PublicDashboardListResponse, isPaused: boolean) => { const req = { dashboard: { uid: pd.dashboardUid }, payload: { @@ -118,20 +129,33 @@ const PublicDashboardCard = ({ pd }: { pd: ListPublicDashboardResponse }) => { }; export const PublicDashboardListTable = () => { - const styles = useStyles2(getStyles); + const [page, setPage] = useState(1); - const { data: publicDashboards, isLoading, isFetching } = useListPublicDashboardsQuery(); + const styles = useStyles2(getStyles); + const { data: paginatedPublicDashboards, isLoading, isFetching, isError } = useListPublicDashboardsQuery(page); return ( }> -
    - {publicDashboards?.map((pd: ListPublicDashboardResponse) => ( -
  • - -
  • - ))} -
+ {!isLoading && !isError && !!paginatedPublicDashboards && ( +
+
    + {paginatedPublicDashboards.publicDashboards.map((pd: PublicDashboardListResponse) => ( +
  • + +
  • + ))} +
+ + + +
+ )}
); @@ -140,6 +164,7 @@ export const PublicDashboardListTable = () => { const getStyles = (theme: GrafanaTheme2) => ({ list: css` list-style-type: none; + margin-bottom: ${theme.spacing(2)}; `, card: css` ${theme.breakpoints.up('sm')} { diff --git a/public/app/features/manage-dashboards/types.ts b/public/app/features/manage-dashboards/types.ts index b33aa67f077..0b24c223403 100644 --- a/public/app/features/manage-dashboards/types.ts +++ b/public/app/features/manage-dashboards/types.ts @@ -18,10 +18,21 @@ export type DeleteDashboardResponse = { title: string; }; -export interface ListPublicDashboardResponse { +export interface PublicDashboardListWithPaginationResponse { + publicDashboards: PublicDashboardListResponse[]; + page: number; + perPage: number; + totalCount: number; +} + +export interface PublicDashboardListResponse { uid: string; accessToken: string; dashboardUid: string; title: string; isEnabled: boolean; } + +export interface PublicDashboardListWithPagination extends PublicDashboardListWithPaginationResponse { + totalPages: number; +}