mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Folders: Introduce folder service function for fetching folders by org and UIDs that contain optionally the folder full path (#80716)
* Folders: Expose function for getting all org folders with specific UIDs * Return all org folders if UIDs is empty * Filter out not accessible folders by the user * Modify query to optionally returning a string that contains the UIDs of all parent folders separated by slash.
This commit is contained in:
parent
f154b2b855
commit
5e88d29814
@ -121,6 +121,37 @@ func (s *Service) DBMigration(db db.DB) {
|
|||||||
s.log.Debug("syncing dashboard and folder tables finished")
|
s.log.Debug("syncing dashboard and folder tables finished")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||||
|
if q.SignedInUser == nil {
|
||||||
|
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
||||||
|
}
|
||||||
|
|
||||||
|
qry := NewGetFoldersQuery(q)
|
||||||
|
permissions := q.SignedInUser.GetPermissions()
|
||||||
|
folderPermissions := permissions[dashboards.ActionFoldersRead]
|
||||||
|
qry.ancestorUIDs = make([]string, 0, len(folderPermissions))
|
||||||
|
for _, p := range folderPermissions {
|
||||||
|
if p == dashboards.ScopeFoldersAll {
|
||||||
|
// no need to query for folders with permissions
|
||||||
|
// the user has permission to access all folders
|
||||||
|
qry.ancestorUIDs = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if folderUid, found := strings.CutPrefix(p, dashboards.ScopeFoldersPrefix); found {
|
||||||
|
if !slices.Contains(qry.ancestorUIDs, folderUid) {
|
||||||
|
qry.ancestorUIDs = append(qry.ancestorUIDs, folderUid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dashFolders, err := s.store.GetFolders(ctx, qry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashFolders, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||||
if q.SignedInUser == nil {
|
if q.SignedInUser == nil {
|
||||||
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
||||||
@ -357,7 +388,7 @@ func (s *Service) getAvailableNonRootFolders(ctx context.Context, orgID int64, u
|
|||||||
return nonRootFolders, nil
|
return nonRootFolders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dashFolders, err := s.store.GetFolders(ctx, orgID, folderUids)
|
dashFolders, err := s.store.GetFolders(ctx, NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: folderUids}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
|
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -420,11 +421,11 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn)
|
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd)
|
ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd)
|
||||||
|
|
||||||
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
|
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
|
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
_ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
|
_ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
|
||||||
@ -443,7 +444,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
countCmd := folder.GetDescendantCountsQuery{
|
countCmd := folder.GetDescendantCountsQuery{
|
||||||
UID: &ancestorUIDs[0],
|
UID: &ancestors[0].UID,
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
SignedInUser: &signedInUser,
|
SignedInUser: &signedInUser,
|
||||||
}
|
}
|
||||||
@ -456,8 +457,8 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
guardian.New = origNewGuardian
|
guardian.New = origNewGuardian
|
||||||
for _, uid := range ancestorUIDs {
|
for _, ancestor := range ancestors {
|
||||||
err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID)
|
err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -500,11 +501,11 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff)
|
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd)
|
ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd)
|
||||||
|
|
||||||
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
|
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
|
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
_ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
|
_ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
|
||||||
@ -523,7 +524,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
countCmd := folder.GetDescendantCountsQuery{
|
countCmd := folder.GetDescendantCountsQuery{
|
||||||
UID: &ancestorUIDs[0],
|
UID: &ancestors[0].UID,
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
SignedInUser: &signedInUser,
|
SignedInUser: &signedInUser,
|
||||||
}
|
}
|
||||||
@ -536,8 +537,8 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
guardian.New = origNewGuardian
|
guardian.New = origNewGuardian
|
||||||
for _, uid := range ancestorUIDs {
|
for _, ancestor := range ancestors {
|
||||||
err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID)
|
err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -638,9 +639,9 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac)
|
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd)
|
ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd)
|
||||||
|
|
||||||
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
|
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = createRule(t, alertStore, parent.UID, "parent alert")
|
_ = createRule(t, alertStore, parent.UID, "parent alert")
|
||||||
|
|
||||||
@ -649,7 +650,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
subPanel model.LibraryElementDTO
|
subPanel model.LibraryElementDTO
|
||||||
)
|
)
|
||||||
if tc.depth > 1 {
|
if tc.depth > 1 {
|
||||||
subfolder, err = serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
|
subfolder, err = serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = createRule(t, alertStore, subfolder.UID, "sub alert")
|
_ = createRule(t, alertStore, subfolder.UID, "sub alert")
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
@ -663,7 +664,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
deleteCmd := folder.DeleteFolderCommand{
|
deleteCmd := folder.DeleteFolderCommand{
|
||||||
UID: ancestorUIDs[0],
|
UID: ancestors[0].UID,
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
SignedInUser: &signedInUser,
|
SignedInUser: &signedInUser,
|
||||||
ForceDeleteRules: tc.forceDelete,
|
ForceDeleteRules: tc.forceDelete,
|
||||||
@ -672,12 +673,12 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
err = tc.service.Delete(context.Background(), &deleteCmd)
|
err = tc.service.Delete(context.Background(), &deleteCmd)
|
||||||
require.ErrorIs(t, err, tc.deletionErr)
|
require.ErrorIs(t, err, tc.deletionErr)
|
||||||
|
|
||||||
for i, uid := range ancestorUIDs {
|
for i, ancestor := range ancestors {
|
||||||
// dashboard table
|
// dashboard table
|
||||||
_, err := tc.service.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid)
|
_, err := tc.service.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestor.UID)
|
||||||
require.ErrorIs(t, err, tc.dashboardErr)
|
require.ErrorIs(t, err, tc.dashboardErr)
|
||||||
// folder table
|
// folder table
|
||||||
_, err = tc.service.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID})
|
_, err = tc.service.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestors[i].UID, OrgID: orgID})
|
||||||
require.ErrorIs(t, err, tc.folderErr)
|
require.ErrorIs(t, err, tc.folderErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1325,12 +1326,12 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
|
|||||||
CanViewValue: true,
|
CanViewValue: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
ancestorUIDsFolderWithPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withPermissions", createCmd)
|
ancestorFoldersWithPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withPermissions", createCmd)
|
||||||
ancestorUIDsFolderWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd)
|
ancestorFoldersWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd)
|
||||||
|
|
||||||
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDsFolderWithoutPermissions[0])
|
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorFoldersWithoutPermissions[0].UID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDsFolderWithoutPermissions[1])
|
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorFoldersWithoutPermissions[1].UID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
dash1 := insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
|
dash1 := insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
|
||||||
@ -1341,19 +1342,19 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
|
|||||||
CanSaveValue: true,
|
CanSaveValue: true,
|
||||||
CanViewValue: true,
|
CanViewValue: true,
|
||||||
CanViewUIDs: []string{
|
CanViewUIDs: []string{
|
||||||
ancestorUIDsFolderWithPermissions[0],
|
ancestorFoldersWithPermissions[0].UID,
|
||||||
ancestorUIDsFolderWithPermissions[1],
|
ancestorFoldersWithPermissions[1].UID,
|
||||||
ancestorUIDsFolderWithoutPermissions[1],
|
ancestorFoldersWithoutPermissions[1].UID,
|
||||||
dash1.UID,
|
dash1.UID,
|
||||||
dash2.UID,
|
dash2.UID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{
|
signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{
|
||||||
dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithPermissions[0]),
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithPermissions[0].UID),
|
||||||
// Add permission to the subfolder of folder with permission (to check deduplication)
|
// Add permission to the subfolder of folder with permission (to check deduplication)
|
||||||
dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithPermissions[1]),
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithPermissions[1].UID),
|
||||||
// Add permission to the subfolder of folder without permission
|
// Add permission to the subfolder of folder without permission
|
||||||
dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorUIDsFolderWithoutPermissions[1]),
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithoutPermissions[1].UID),
|
||||||
}
|
}
|
||||||
signedInUser.Permissions[orgID][dashboards.ActionDashboardsRead] = []string{
|
signedInUser.Permissions[orgID][dashboards.ActionDashboardsRead] = []string{
|
||||||
dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash1.UID),
|
dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash1.UID),
|
||||||
@ -1374,8 +1375,8 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sharedFolders, 1)
|
require.Len(t, sharedFolders, 1)
|
||||||
require.Contains(t, sharedFoldersUIDs, ancestorUIDsFolderWithoutPermissions[1])
|
require.Contains(t, sharedFoldersUIDs, ancestorFoldersWithoutPermissions[1].UID)
|
||||||
require.NotContains(t, sharedFoldersUIDs, ancestorUIDsFolderWithPermissions[1])
|
require.NotContains(t, sharedFoldersUIDs, ancestorFoldersWithPermissions[1].UID)
|
||||||
|
|
||||||
sharedDashboards, err := dashboardService.GetDashboardsSharedWithUser(context.Background(), &signedInUser)
|
sharedDashboards, err := dashboardService.GetDashboardsSharedWithUser(context.Background(), &signedInUser)
|
||||||
sharedDashboardsUIDs := make([]string, 0)
|
sharedDashboardsUIDs := make([]string, 0)
|
||||||
@ -1390,28 +1391,236 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
|
|||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
guardian.New = origNewGuardian
|
guardian.New = origNewGuardian
|
||||||
for _, uid := range ancestorUIDsFolderWithPermissions {
|
for _, ancestor := range ancestorFoldersWithPermissions {
|
||||||
err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID)
|
err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
guardian.New = origNewGuardian
|
guardian.New = origNewGuardian
|
||||||
for _, uid := range ancestorUIDsFolderWithoutPermissions {
|
for _, ancestor := range ancestorFoldersWithoutPermissions {
|
||||||
err := serviceWithFlagOn.store.Delete(context.Background(), uid, orgID)
|
err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Should get org folders visible", func(t *testing.T) {
|
||||||
|
depth := 3
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
||||||
|
CanSaveValue: true,
|
||||||
|
CanViewValue: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// create folder sctructure like this:
|
||||||
|
// tree1-folder-0
|
||||||
|
// └──tree1-folder-1
|
||||||
|
// └──tree1-folder-2
|
||||||
|
// tree2-folder-0
|
||||||
|
// └──tree2-folder-1
|
||||||
|
// └──tree2-folder-2
|
||||||
|
tree1 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree1-", createCmd)
|
||||||
|
tree2 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree2-", createCmd)
|
||||||
|
|
||||||
|
signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{
|
||||||
|
// Add permission to tree1-folder-0
|
||||||
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree1[0].UID),
|
||||||
|
// Add permission to the subfolder of folder with permission (tree1-folder-1) to check deduplication
|
||||||
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree1[1].UID),
|
||||||
|
// Add permission to the subfolder of folder without permission (tree2-folder-1)
|
||||||
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree2[1].UID),
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
for _, f := range tree1 {
|
||||||
|
err := serviceWithFlagOn.store.Delete(context.Background(), f.UID, orgID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
for _, f := range tree2 {
|
||||||
|
err := serviceWithFlagOn.store.Delete(context.Background(), f.UID, orgID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
cmd folder.GetFoldersQuery
|
||||||
|
expected []*folder.Folder
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should get all org folders visible to the user",
|
||||||
|
cmd: folder.GetFoldersQuery{
|
||||||
|
OrgID: orgID,
|
||||||
|
SignedInUser: &signedInUser,
|
||||||
|
},
|
||||||
|
expected: []*folder.Folder{
|
||||||
|
{
|
||||||
|
UID: tree1[0].UID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree1[1].UID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree1[2].UID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[1].UID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[2].UID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should get all org folders visible to the user with fullpath",
|
||||||
|
cmd: folder.GetFoldersQuery{
|
||||||
|
OrgID: orgID,
|
||||||
|
WithFullpath: true,
|
||||||
|
SignedInUser: &signedInUser,
|
||||||
|
},
|
||||||
|
expected: []*folder.Folder{
|
||||||
|
{
|
||||||
|
UID: tree1[0].UID,
|
||||||
|
Fullpath: "tree1-folder-0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree1[1].UID,
|
||||||
|
Fullpath: "tree1-folder-0/tree1-folder-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree1[2].UID,
|
||||||
|
Fullpath: "tree1-folder-0/tree1-folder-1/tree1-folder-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[1].UID,
|
||||||
|
Fullpath: "tree2-folder-0/tree2-folder-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[2].UID,
|
||||||
|
Fullpath: "tree2-folder-0/tree2-folder-1/tree2-folder-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should get all org folders visible to the user with fullpath UIDs",
|
||||||
|
cmd: folder.GetFoldersQuery{
|
||||||
|
OrgID: orgID,
|
||||||
|
WithFullpathUIDs: true,
|
||||||
|
SignedInUser: &signedInUser,
|
||||||
|
},
|
||||||
|
expected: []*folder.Folder{
|
||||||
|
{
|
||||||
|
UID: tree1[0].UID,
|
||||||
|
FullpathUIDs: strings.Join([]string{tree1[0].UID}, "/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree1[1].UID,
|
||||||
|
FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID}, "/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree1[2].UID,
|
||||||
|
FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID, tree1[2].UID}, "/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[1].UID,
|
||||||
|
FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID}, "/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[2].UID,
|
||||||
|
FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID, tree2[2].UID}, "/"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should get specific org folders visible to the user",
|
||||||
|
cmd: folder.GetFoldersQuery{
|
||||||
|
OrgID: orgID,
|
||||||
|
UIDs: []string{tree1[0].UID, tree2[0].UID, tree2[1].UID},
|
||||||
|
SignedInUser: &signedInUser,
|
||||||
|
},
|
||||||
|
expected: []*folder.Folder{
|
||||||
|
{
|
||||||
|
UID: tree1[0].UID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[1].UID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should get all org folders visible to the user with admin permissions",
|
||||||
|
cmd: folder.GetFoldersQuery{
|
||||||
|
OrgID: orgID,
|
||||||
|
SignedInUser: &signedInAdminUser,
|
||||||
|
},
|
||||||
|
expected: []*folder.Folder{
|
||||||
|
{
|
||||||
|
UID: tree1[0].UID,
|
||||||
|
Fullpath: "tree1-folder-0",
|
||||||
|
FullpathUIDs: strings.Join([]string{tree1[0].UID}, "/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree1[1].UID,
|
||||||
|
Fullpath: "tree1-folder-0/tree1-folder-1",
|
||||||
|
FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID}, "/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree1[2].UID,
|
||||||
|
Fullpath: "tree1-folder-0/tree1-folder-1/tree1-folder-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[0].UID,
|
||||||
|
Fullpath: "tree2-folder-0",
|
||||||
|
FullpathUIDs: strings.Join([]string{tree2[0].UID}, "/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[1].UID,
|
||||||
|
Fullpath: "tree2-folder-0/tree2-folder-1",
|
||||||
|
FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID}, "/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: tree2[2].UID,
|
||||||
|
Fullpath: "tree2-folder-0/tree2-folder-1/tree2-folder-2",
|
||||||
|
FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID, tree2[2].UID}, "/"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
actualFolders, err := serviceWithFlagOn.GetFolders(context.Background(), tc.cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, actualFolders, len(tc.expected))
|
||||||
|
|
||||||
|
for i, expected := range tc.expected {
|
||||||
|
actualFolder := actualFolders[i]
|
||||||
|
require.Equal(t, expected.UID, actualFolder.UID)
|
||||||
|
if tc.cmd.WithFullpath {
|
||||||
|
require.Equal(t, expected.Fullpath, actualFolder.Fullpath)
|
||||||
|
} else {
|
||||||
|
require.Empty(t, actualFolder.Fullpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.cmd.WithFullpathUIDs {
|
||||||
|
require.Equal(t, expected.FullpathUIDs, actualFolder.FullpathUIDs)
|
||||||
|
} else {
|
||||||
|
require.Empty(t, actualFolder.FullpathUIDs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []string {
|
func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
ancestorUIDs := []string{}
|
folders := make([]*folder.Folder, 0, depth)
|
||||||
if cmd.ParentUID != "" {
|
|
||||||
ancestorUIDs = append(ancestorUIDs, cmd.ParentUID)
|
|
||||||
}
|
|
||||||
for i := 0; i < depth; i++ {
|
for i := 0; i < depth; i++ {
|
||||||
title := fmt.Sprintf("%sfolder-%d", prefix, i)
|
title := fmt.Sprintf("%sfolder-%d", prefix, i)
|
||||||
cmd.Title = title
|
cmd.Title = title
|
||||||
@ -1422,23 +1631,12 @@ func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth
|
|||||||
require.Equal(t, title, f.Title)
|
require.Equal(t, title, f.Title)
|
||||||
require.NotEmpty(t, f.UID)
|
require.NotEmpty(t, f.UID)
|
||||||
|
|
||||||
parents, err := store.GetParents(context.Background(), folder.GetParentsQuery{
|
folders = append(folders, f)
|
||||||
UID: f.UID,
|
|
||||||
OrgID: cmd.OrgID,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
parentUIDs := []string{}
|
|
||||||
for _, p := range parents {
|
|
||||||
parentUIDs = append(parentUIDs, p.UID)
|
|
||||||
}
|
|
||||||
require.Equal(t, ancestorUIDs, parentUIDs)
|
|
||||||
|
|
||||||
ancestorUIDs = append(ancestorUIDs, f.UID)
|
|
||||||
|
|
||||||
cmd.ParentUID = f.UID
|
cmd.ParentUID = f.UID
|
||||||
}
|
}
|
||||||
|
|
||||||
return ancestorUIDs
|
return folders
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, db db.DB) folder.Service {
|
func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, db db.DB) folder.Service {
|
||||||
|
@ -2,6 +2,7 @@ package folderimpl
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -12,10 +13,13 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DEFAULT_BATCH_SIZE = 999
|
||||||
|
|
||||||
type sqlStore struct {
|
type sqlStore struct {
|
||||||
db db.DB
|
db db.DB
|
||||||
log log.Logger
|
log log.Logger
|
||||||
@ -342,27 +346,176 @@ func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64,
|
|||||||
return height, nil
|
return height, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *sqlStore) GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error) {
|
// GetFolders returns org folders by their UIDs.
|
||||||
if len(uids) == 0 {
|
// If UIDs is empty, it returns all folders in the org.
|
||||||
return []*folder.Folder{}, nil
|
// If WithFullpath is true it computes also the full path of a folder.
|
||||||
|
// The full path is a string that contains the titles of all parent folders separated by a slash.
|
||||||
|
// For example, if the folder structure is:
|
||||||
|
//
|
||||||
|
// A
|
||||||
|
// └── B
|
||||||
|
// └── C
|
||||||
|
//
|
||||||
|
// The full path of C is "A/B/C".
|
||||||
|
// The full path of B is "A/B".
|
||||||
|
// The full path of A is "A".
|
||||||
|
// If a folder contains a slash in its title, it is escaped with a backslash.
|
||||||
|
// For example, if the folder structure is:
|
||||||
|
//
|
||||||
|
// A
|
||||||
|
// └── B/C
|
||||||
|
//
|
||||||
|
// The full path of C is "A/B\/C".
|
||||||
|
//
|
||||||
|
// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
|
||||||
|
// For example, if the folder structure is:
|
||||||
|
//
|
||||||
|
// A (uid: "uid1")
|
||||||
|
// └── B (uid: "uid2")
|
||||||
|
// └── C (uid: "uid3")
|
||||||
|
//
|
||||||
|
// The full path UIDs of C is "uid1/uid2/uid3".
|
||||||
|
// The full path UIDs of B is "uid1/uid2".
|
||||||
|
// The full path UIDs of A is "uid1".
|
||||||
|
func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) {
|
||||||
|
if q.BatchSize == 0 {
|
||||||
|
q.BatchSize = DEFAULT_BATCH_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
var folders []*folder.Folder
|
var folders []*folder.Folder
|
||||||
if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
b := strings.Builder{}
|
return batch(len(q.UIDs), int(q.BatchSize), func(start, end int) error {
|
||||||
b.WriteString(`SELECT * FROM folder WHERE org_id=? AND uid IN (?` + strings.Repeat(", ?", len(uids)-1) + `)`)
|
partialFolders := make([]*folder.Folder, 0, q.BatchSize)
|
||||||
args := []any{orgID}
|
partialUIDs := q.UIDs[start:min(end, len(q.UIDs))]
|
||||||
for _, uid := range uids {
|
s := strings.Builder{}
|
||||||
|
s.WriteString(`SELECT f0.id, f0.org_id, f0.uid, f0.parent_uid, f0.title, f0.description, f0.created, f0.updated`)
|
||||||
|
// compute full path column if requested
|
||||||
|
if q.WithFullpath {
|
||||||
|
s.WriteString(fmt.Sprintf(`, %s AS fullpath`, getFullpathSQL(ss.db.GetDialect())))
|
||||||
|
}
|
||||||
|
// compute full path UIDs column if requested
|
||||||
|
if q.WithFullpathUIDs {
|
||||||
|
s.WriteString(fmt.Sprintf(`, %s AS fullpath_uids`, getFullapathUIDsSQL(ss.db.GetDialect())))
|
||||||
|
}
|
||||||
|
s.WriteString(` FROM folder f0`)
|
||||||
|
// join the same table multiple times to compute the full path of a folder
|
||||||
|
if q.WithFullpath || q.WithFullpathUIDs || len(q.ancestorUIDs) > 0 {
|
||||||
|
s.WriteString(getFullpathJoinsSQL())
|
||||||
|
}
|
||||||
|
s.WriteString(` WHERE f0.org_id=?`)
|
||||||
|
args := []any{q.OrgID}
|
||||||
|
if len(partialUIDs) > 0 {
|
||||||
|
s.WriteString(` AND f0.uid IN (?` + strings.Repeat(", ?", len(partialUIDs)-1) + `)`)
|
||||||
|
for _, uid := range partialUIDs {
|
||||||
args = append(args, uid)
|
args = append(args, uid)
|
||||||
}
|
}
|
||||||
return sess.SQL(b.String(), args...).Find(&folders)
|
}
|
||||||
|
|
||||||
|
if len(q.ancestorUIDs) == 0 {
|
||||||
|
err := sess.SQL(s.String(), args...).Find(&partialFolders)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
folders = append(folders, partialFolders...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out folders if they are not in the subtree of the given ancestor folders
|
||||||
|
if err := batch(len(q.ancestorUIDs), int(q.BatchSize), func(start2, end2 int) error {
|
||||||
|
s2, args2 := getAncestorsSQL(ss.db.GetDialect(), q.ancestorUIDs, start2, end2, s.String(), args)
|
||||||
|
err := sess.SQL(s2, args2...).Find(&partialFolders)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
folders = append(folders, partialFolders...)
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add URLs
|
// Add URLs
|
||||||
for i, f := range folders {
|
for i, f := range folders {
|
||||||
|
f.Fullpath = strings.TrimLeft(f.Fullpath, "/")
|
||||||
|
f.FullpathUIDs = strings.TrimLeft(f.FullpathUIDs, "/")
|
||||||
folders[i] = f.WithURL()
|
folders[i] = f.WithURL()
|
||||||
}
|
}
|
||||||
|
|
||||||
return folders, nil
|
return folders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getFullpathSQL(dialect migrator.Dialect) string {
|
||||||
|
concatCols := make([]string, 0, folder.MaxNestedFolderDepth)
|
||||||
|
concatCols = append(concatCols, "COALESCE(REPLACE(f0.title, '/', '\\/'), '')")
|
||||||
|
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
|
||||||
|
concatCols = append([]string{fmt.Sprintf("COALESCE(REPLACE(f%d.title, '/', '\\/'), '')", i), "'/'"}, concatCols...)
|
||||||
|
}
|
||||||
|
return dialect.Concat(concatCols...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFullapathUIDsSQL(dialect migrator.Dialect) string {
|
||||||
|
concatCols := make([]string, 0, folder.MaxNestedFolderDepth)
|
||||||
|
concatCols = append(concatCols, "COALESCE(f0.uid, '')")
|
||||||
|
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
|
||||||
|
concatCols = append([]string{fmt.Sprintf("COALESCE(f%d.uid, '')", i), "'/'"}, concatCols...)
|
||||||
|
}
|
||||||
|
return dialect.Concat(concatCols...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFullpathJoinsSQL returns a SQL fragment that joins the same table multiple times to get the full path of a folder.
|
||||||
|
func getFullpathJoinsSQL() string {
|
||||||
|
joins := make([]string, 0, folder.MaxNestedFolderDepth)
|
||||||
|
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
|
||||||
|
joins = append(joins, fmt.Sprintf(` LEFT JOIN folder f%d ON f%d.org_id = f%d.org_id AND f%d.uid = f%d.parent_uid`, i, i, i-1, i, i-1))
|
||||||
|
}
|
||||||
|
return strings.Join(joins, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAncestorsSQL(dialect migrator.Dialect, ancestorUIDs []string, start int, end int, origSQL string, origArgs []any) (string, []any) {
|
||||||
|
s2 := strings.Builder{}
|
||||||
|
s2.WriteString(origSQL)
|
||||||
|
args2 := make([]any, 0, len(ancestorUIDs)*folder.MaxNestedFolderDepth)
|
||||||
|
args2 = append(args2, origArgs...)
|
||||||
|
|
||||||
|
partialAncestorUIDs := ancestorUIDs[start:min(end, len(ancestorUIDs))]
|
||||||
|
partialArgs := make([]any, 0, len(partialAncestorUIDs))
|
||||||
|
for _, uid := range partialAncestorUIDs {
|
||||||
|
partialArgs = append(partialArgs, uid)
|
||||||
|
}
|
||||||
|
s2.WriteString(` AND ( f0.uid IN (?` + strings.Repeat(", ?", len(partialAncestorUIDs)-1) + `)`)
|
||||||
|
args2 = append(args2, partialArgs...)
|
||||||
|
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
|
||||||
|
s2.WriteString(fmt.Sprintf(` OR f%d.uid IN (?`+strings.Repeat(", ?", len(partialAncestorUIDs)-1)+`)`, i))
|
||||||
|
args2 = append(args2, partialArgs...)
|
||||||
|
}
|
||||||
|
s2.WriteString(` )`)
|
||||||
|
return s2.String(), args2
|
||||||
|
}
|
||||||
|
|
||||||
|
func batch(count, batchSize int, eachFn func(start, end int) error) error {
|
||||||
|
if count == 0 {
|
||||||
|
if err := eachFn(0, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < count; {
|
||||||
|
end := i + batchSize
|
||||||
|
if end > count {
|
||||||
|
end = count
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := eachFn(i, end); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i = end
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -759,24 +760,91 @@ func TestIntegrationGetFolders(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get folders by UIDs should succeed", func(t *testing.T) {
|
t.Run("get folders by UIDs should succeed", func(t *testing.T) {
|
||||||
ff, err := folderStore.GetFolders(context.Background(), orgID, uids)
|
actualFolders, err := folderStore.GetFolders(context.Background(), NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:]}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, len(uids), len(ff))
|
assert.Equal(t, len(uids[1:]), len(actualFolders))
|
||||||
for _, f := range folders {
|
for _, f := range folders[1:] {
|
||||||
folderInResponseIdx := slices.IndexFunc(ff, func(rf *folder.Folder) bool {
|
folderInResponseIdx := slices.IndexFunc(actualFolders, func(rf *folder.Folder) bool {
|
||||||
return rf.UID == f.UID
|
return rf.UID == f.UID
|
||||||
})
|
})
|
||||||
assert.NotEqual(t, -1, folderInResponseIdx)
|
assert.NotEqual(t, -1, folderInResponseIdx)
|
||||||
rf := ff[folderInResponseIdx]
|
actualFolder := actualFolders[folderInResponseIdx]
|
||||||
assert.Equal(t, f.UID, rf.UID)
|
assert.Equal(t, f.UID, actualFolder.UID)
|
||||||
assert.Equal(t, f.OrgID, rf.OrgID)
|
assert.Equal(t, f.OrgID, actualFolder.OrgID)
|
||||||
assert.Equal(t, f.Title, rf.Title)
|
assert.Equal(t, f.Title, actualFolder.Title)
|
||||||
assert.Equal(t, f.Description, rf.Description)
|
assert.Equal(t, f.Description, actualFolder.Description)
|
||||||
assert.NotEmpty(t, rf.Created)
|
assert.NotEmpty(t, actualFolder.Created)
|
||||||
assert.NotEmpty(t, rf.Updated)
|
assert.NotEmpty(t, actualFolder.Updated)
|
||||||
assert.NotEmpty(t, rf.URL)
|
assert.NotEmpty(t, actualFolder.URL)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("get folders by UIDs batching should work as expected", func(t *testing.T) {
|
||||||
|
q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], BatchSize: 3})
|
||||||
|
actualFolders, err := folderStore.GetFolders(context.Background(), q)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, len(uids[1:]), len(actualFolders))
|
||||||
|
for _, f := range folders[1:] {
|
||||||
|
folderInResponseIdx := slices.IndexFunc(actualFolders, func(rf *folder.Folder) bool {
|
||||||
|
return rf.UID == f.UID
|
||||||
|
})
|
||||||
|
assert.NotEqual(t, -1, folderInResponseIdx)
|
||||||
|
actualFolder := actualFolders[folderInResponseIdx]
|
||||||
|
assert.Equal(t, f.UID, actualFolder.UID)
|
||||||
|
assert.Equal(t, f.OrgID, actualFolder.OrgID)
|
||||||
|
assert.Equal(t, f.Title, actualFolder.Title)
|
||||||
|
assert.Equal(t, f.Description, actualFolder.Description)
|
||||||
|
assert.NotEmpty(t, actualFolder.Created)
|
||||||
|
assert.NotEmpty(t, actualFolder.Updated)
|
||||||
|
assert.NotEmpty(t, actualFolder.URL)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get folders by UIDs with fullpath should succeed", func(t *testing.T) {
|
||||||
|
q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], WithFullpath: true})
|
||||||
|
q.BatchSize = 3
|
||||||
|
actualFolders, err := folderStore.GetFolders(context.Background(), q)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, len(uids[1:]), len(actualFolders))
|
||||||
|
for _, f := range folders[1:] {
|
||||||
|
folderInResponseIdx := slices.IndexFunc(actualFolders, func(rf *folder.Folder) bool {
|
||||||
|
return rf.UID == f.UID
|
||||||
|
})
|
||||||
|
assert.NotEqual(t, -1, folderInResponseIdx)
|
||||||
|
actualFolder := actualFolders[folderInResponseIdx]
|
||||||
|
assert.Equal(t, f.UID, actualFolder.UID)
|
||||||
|
assert.Equal(t, f.OrgID, actualFolder.OrgID)
|
||||||
|
assert.Equal(t, f.Title, actualFolder.Title)
|
||||||
|
assert.Equal(t, f.Description, actualFolder.Description)
|
||||||
|
assert.NotEmpty(t, actualFolder.Created)
|
||||||
|
assert.NotEmpty(t, actualFolder.Updated)
|
||||||
|
assert.NotEmpty(t, actualFolder.URL)
|
||||||
|
assert.NotEmpty(t, actualFolder.Fullpath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get folders by UIDs and ancestor UIDs should work as expected", func(t *testing.T) {
|
||||||
|
q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], BatchSize: 3})
|
||||||
|
q.ancestorUIDs = make([]string, 0, int(q.BatchSize)+1)
|
||||||
|
for i := 0; i < int(q.BatchSize); i++ {
|
||||||
|
q.ancestorUIDs = append(q.ancestorUIDs, uuid.New().String())
|
||||||
|
}
|
||||||
|
q.ancestorUIDs = append(q.ancestorUIDs, folders[len(folders)-1].UID)
|
||||||
|
|
||||||
|
actualFolders, err := folderStore.GetFolders(context.Background(), q)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(actualFolders))
|
||||||
|
|
||||||
|
f := folders[len(folders)-1]
|
||||||
|
actualFolder := actualFolders[0]
|
||||||
|
assert.Equal(t, f.UID, actualFolder.UID)
|
||||||
|
assert.Equal(t, f.OrgID, actualFolder.OrgID)
|
||||||
|
assert.Equal(t, f.Title, actualFolder.Title)
|
||||||
|
assert.Equal(t, f.Description, actualFolder.Description)
|
||||||
|
assert.NotEmpty(t, actualFolder.Created)
|
||||||
|
assert.NotEmpty(t, actualFolder.Updated)
|
||||||
|
assert.NotEmpty(t, actualFolder.URL)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 {
|
func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 {
|
||||||
|
@ -6,6 +6,18 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type getFoldersQuery struct {
|
||||||
|
folder.GetFoldersQuery
|
||||||
|
ancestorUIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetFoldersQuery(q folder.GetFoldersQuery) getFoldersQuery {
|
||||||
|
return getFoldersQuery{
|
||||||
|
GetFoldersQuery: q,
|
||||||
|
ancestorUIDs: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// store is the interface which a folder store must implement.
|
// store is the interface which a folder store must implement.
|
||||||
type store interface {
|
type store interface {
|
||||||
// Create creates a folder and returns the newly-created folder.
|
// Create creates a folder and returns the newly-created folder.
|
||||||
@ -35,5 +47,5 @@ type store interface {
|
|||||||
GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error)
|
GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error)
|
||||||
|
|
||||||
// GetFolders returns folders with given uids
|
// GetFolders returns folders with given uids
|
||||||
GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error)
|
GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error)
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,6 @@ func (f *fakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64
|
|||||||
return f.ExpectedFolderHeight, f.ExpectedError
|
return f.ExpectedFolderHeight, f.ExpectedError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeStore) GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error) {
|
func (f *fakeStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) {
|
||||||
return f.ExpectedFolders, f.ExpectedError
|
return f.ExpectedFolders, f.ExpectedError
|
||||||
}
|
}
|
||||||
|
@ -51,3 +51,7 @@ func (s *FakeService) RegisterService(service folder.RegistryService) error {
|
|||||||
func (s *FakeService) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
|
func (s *FakeService) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
|
||||||
return s.ExpectedDescendantCounts, s.ExpectedError
|
return s.ExpectedDescendantCounts, s.ExpectedError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FakeService) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||||
|
return s.ExpectedFolders, s.ExpectedError
|
||||||
|
}
|
||||||
|
@ -46,6 +46,8 @@ type Folder struct {
|
|||||||
UpdatedBy int64
|
UpdatedBy int64
|
||||||
CreatedBy int64
|
CreatedBy int64
|
||||||
HasACL bool
|
HasACL bool
|
||||||
|
Fullpath string `xorm:"fullpath"`
|
||||||
|
FullpathUIDs string `xorm:"fullpath_uids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var GeneralFolder = Folder{ID: 0, Title: "General"}
|
var GeneralFolder = Folder{ID: 0, Title: "General"}
|
||||||
@ -149,6 +151,16 @@ type GetFolderQuery struct {
|
|||||||
SignedInUser identity.Requester `json:"-"`
|
SignedInUser identity.Requester `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetFoldersQuery struct {
|
||||||
|
OrgID int64
|
||||||
|
UIDs []string
|
||||||
|
WithFullpath bool
|
||||||
|
WithFullpathUIDs bool
|
||||||
|
BatchSize uint64
|
||||||
|
|
||||||
|
SignedInUser identity.Requester `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetParentsQuery captures the information required by the folder service to
|
// GetParentsQuery captures the information required by the folder service to
|
||||||
// return a list of all parent folders of a given folder.
|
// return a list of all parent folders of a given folder.
|
||||||
type GetParentsQuery struct {
|
type GetParentsQuery struct {
|
||||||
|
@ -25,6 +25,12 @@ type Service interface {
|
|||||||
// Move changes a folder's parent folder to the requested new parent.
|
// Move changes a folder's parent folder to the requested new parent.
|
||||||
Move(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
|
Move(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
|
||||||
RegisterService(service RegistryService) error
|
RegisterService(service RegistryService) error
|
||||||
|
// GetFolders returns org folders that are accessible by the signed in user by their UIDs.
|
||||||
|
// If WithFullpath is true it computes also the full path of a folder.
|
||||||
|
// The full path is a string that contains the titles of all parent folders separated by a slash.
|
||||||
|
// If a folder contains a slash in its title, it is escaped with a backslash.
|
||||||
|
// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
|
||||||
|
GetFolders(ctx context.Context, q GetFoldersQuery) ([]*Folder, error)
|
||||||
GetDescendantCounts(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
|
GetDescendantCounts(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +91,10 @@ type Dialect interface {
|
|||||||
// column names to values to use in the where clause.
|
// column names to values to use in the where clause.
|
||||||
// The update is executed as part of the provided session.
|
// The update is executed as part of the provided session.
|
||||||
Update(ctx context.Context, tx *session.SessionTx, tableName string, row map[string]any, where map[string]any) error
|
Update(ctx context.Context, tx *session.SessionTx, tableName string, row map[string]any, where map[string]any) error
|
||||||
|
// Concat returns the sql statement for concating multiple strings
|
||||||
|
// Implementations are not expected to quote the arguments
|
||||||
|
// therefore any callers should take care to quote arguments as necessary
|
||||||
|
Concat(...string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LockCfg struct {
|
type LockCfg struct {
|
||||||
@ -450,3 +454,7 @@ func (b *BaseDialect) Update(ctx context.Context, tx *session.SessionTx, tableNa
|
|||||||
_, err = tx.Exec(ctx, query, args...)
|
_, err = tx.Exec(ctx, query, args...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDialect) Concat(strs ...string) string {
|
||||||
|
return fmt.Sprintf("CONCAT(%s)", strings.Join(strs, ", "))
|
||||||
|
}
|
||||||
|
@ -209,3 +209,7 @@ func (db *SQLite3) UpsertMultipleSQL(tableName string, keyCols, updateCols []str
|
|||||||
)
|
)
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *SQLite3) Concat(strs ...string) string {
|
||||||
|
return strings.Join(strs, " || ")
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user