Nested folders: Fetch multiple folders from dashboard folder store (#72464)

This commit is contained in:
Sofia Papagiannaki 2023-08-01 11:04:44 +03:00 committed by GitHub
parent 7612f3d955
commit 1869da1d86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 14 deletions

View File

@ -2,6 +2,7 @@ package folderimpl
import (
"context"
"strings"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -84,3 +85,38 @@ func (d *DashboardFolderStoreImpl) GetFolderByUID(ctx context.Context, orgID int
}
return dashboards.FromDashboard(&dashboard), nil
}
func (d *DashboardFolderStoreImpl) GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*folder.Folder, error) {
m := make(map[string]*folder.Folder, len(uids))
var folders []*folder.Folder
if err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
b := strings.Builder{}
args := make([]interface{}, 0, len(uids)+1)
b.WriteString("SELECT * FROM dashboard WHERE org_id=? ")
args = append(args, orgID)
for i, uid := range uids {
if i == 0 {
b.WriteString(" AND (")
}
if i > 0 {
b.WriteString(" OR ")
}
b.WriteString(" uid=? ")
args = append(args, uid)
if i == len(uids)-1 {
b.WriteString(")")
}
}
return sess.SQL(b.String(), args...).Find(&folders)
}); err != nil {
return nil, err
}
for _, f := range folders {
m[f.UID] = f
}
return m, nil
}

View File

@ -184,14 +184,25 @@ func (s *Service) GetChildren(ctx context.Context, cmd *folder.GetChildrenQuery)
return nil, err
}
childrenUIDs := make([]string, 0, len(children))
for _, f := range children {
childrenUIDs = append(childrenUIDs, f.UID)
}
dashFolders, err := s.dashboardFolderStore.GetFolders(ctx, cmd.OrgID, childrenUIDs)
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders from dashboard store: %w", err)
}
filtered := make([]*folder.Folder, 0, len(children))
for _, f := range children {
// fetch folder from dashboard store
dashFolder, err := s.dashboardFolderStore.GetFolderByUID(ctx, f.OrgID, f.UID)
if err != nil {
s.log.Error("failed to fetch folder by UID from dashboard store", "uid", f.UID, "error", err)
dashFolder, ok := dashFolders[f.UID]
if !ok {
s.log.Error("failed to fetch folder by UID from dashboard store", "uid", f.UID)
continue
}
// always expose the dashboard store sequential ID
f.ID = dashFolder.ID
@ -500,9 +511,14 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e
result = append(result, subfolders...)
}
dashFolders, err := s.dashboardFolderStore.GetFolders(ctx, cmd.OrgID, result)
if err != nil {
return folder.ErrInternal.Errorf("failed to fetch subfolders from dashboard store: %w", err)
}
for _, folder := range result {
dashFolder, err := s.dashboardFolderStore.GetFolderByUID(ctx, cmd.OrgID, folder)
if err != nil {
dashFolder, ok := dashFolders[folder]
if !ok {
return err
}

View File

@ -240,7 +240,7 @@ func TestIntegrationFolderService(t *testing.T) {
f := folder.NewFolder(util.GenerateShortUID(), "")
f.ID = rand.Int63()
f.UID = util.GenerateShortUID()
folderStore.On("GetFolderByUID", mock.Anything, orgID, f.UID).Return(f, nil)
folderStore.On("GetFolders", mock.Anything, orgID, []string{f.UID}).Return(map[string]*folder.Folder{f.UID: f}, nil)
var actualCmd *dashboards.DeleteDashboardCommand
dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.16.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package foldertest
@ -19,6 +19,10 @@ func (_m *FakeFolderStore) GetFolderByID(ctx context.Context, orgID int64, id in
ret := _m.Called(ctx, orgID, id)
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*folder.Folder, error)); ok {
return rf(ctx, orgID, id)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *folder.Folder); ok {
r0 = rf(ctx, orgID, id)
} else {
@ -27,7 +31,6 @@ func (_m *FakeFolderStore) GetFolderByID(ctx context.Context, orgID int64, id in
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok {
r1 = rf(ctx, orgID, id)
} else {
@ -42,6 +45,10 @@ func (_m *FakeFolderStore) GetFolderByTitle(ctx context.Context, orgID int64, ti
ret := _m.Called(ctx, orgID, title)
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*folder.Folder, error)); ok {
return rf(ctx, orgID, title)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *folder.Folder); ok {
r0 = rf(ctx, orgID, title)
} else {
@ -50,7 +57,6 @@ func (_m *FakeFolderStore) GetFolderByTitle(ctx context.Context, orgID int64, ti
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgID, title)
} else {
@ -65,6 +71,10 @@ func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid
ret := _m.Called(ctx, orgID, uid)
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*folder.Folder, error)); ok {
return rf(ctx, orgID, uid)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *folder.Folder); ok {
r0 = rf(ctx, orgID, uid)
} else {
@ -73,7 +83,6 @@ func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgID, uid)
} else {
@ -83,13 +92,38 @@ func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid
return r0, r1
}
type mockConstructorTestingTNewFakeFolderStore interface {
mock.TestingT
Cleanup(func())
// GetFolders provides a mock function with given fields: ctx, orgID, uids
func (_m *FakeFolderStore) GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*folder.Folder, error) {
ret := _m.Called(ctx, orgID, uids)
var r0 map[string]*folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, []string) (map[string]*folder.Folder, error)); ok {
return rf(ctx, orgID, uids)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, []string) map[string]*folder.Folder); ok {
r0 = rf(ctx, orgID, uids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*folder.Folder)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64, []string) error); ok {
r1 = rf(ctx, orgID, uids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewFakeFolderStore creates a new instance of FakeFolderStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewFakeFolderStore(t mockConstructorTestingTNewFakeFolderStore) *FakeFolderStore {
// The first argument is typically a *testing.T value.
func NewFakeFolderStore(t interface {
mock.TestingT
Cleanup(func())
}) *FakeFolderStore {
mock := &FakeFolderStore{}
mock.Mock.Test(t)

View File

@ -39,4 +39,6 @@ type FolderStore interface {
GetFolderByUID(ctx context.Context, orgID int64, uid string) (*Folder, error)
// GetFolderByID retrieves a folder by its ID
GetFolderByID(ctx context.Context, orgID int64, id int64) (*Folder, error)
// GetFolders returns all folders for the given orgID and UIDs.
GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*Folder, error)
}