NestedFolders: Return full folder hierarchy in Folder response (#66835)

* Delete redundant struct

* Include parent folders in DTO

* Add test

* Update swagger
This commit is contained in:
Sofia Papagiannaki 2023-04-25 11:22:20 +03:00 committed by GitHub
parent 5c2a344ce1
commit 7dbcd5ecd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 153 additions and 36 deletions

View File

@ -24,6 +24,8 @@ type Folder struct {
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
// only used if nested folders are enabled // only used if nested folders are enabled
ParentUID string `json:"parentUid,omitempty"` ParentUID string `json:"parentUid,omitempty"`
// the parent folders starting from the root going down
Parents []Folder `json:"parents,omitempty"`
} }
type FolderSearchHit struct { type FolderSearchHit struct {

View File

@ -316,42 +316,63 @@ func (hs *HTTPServer) GetFolderChildrenCounts(c *contextmodel.ReqContext) respon
return response.JSON(http.StatusOK, counts) return response.JSON(http.StatusOK, counts)
} }
func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, g guardian.DashboardGuardian, f *folder.Folder) dtos.Folder {
ctx := c.Req.Context()
toDTO := func(f *folder.Folder) dtos.Folder {
canEdit, _ := g.CanEdit()
canSave, _ := g.CanSave()
canAdmin, _ := g.CanAdmin()
canDelete, _ := g.CanDelete()
func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, g guardian.DashboardGuardian, folder *folder.Folder) dtos.Folder { // Finding creator and last updater of the folder
canEdit, _ := g.CanEdit() updater, creator := anonString, anonString
canSave, _ := g.CanSave() if f.CreatedBy > 0 {
canAdmin, _ := g.CanAdmin() creator = hs.getUserLogin(ctx, f.CreatedBy)
canDelete, _ := g.CanDelete() }
if f.UpdatedBy > 0 {
updater = hs.getUserLogin(ctx, f.UpdatedBy)
}
// Finding creator and last updater of the folder acMetadata, _ := hs.getFolderACMetadata(c, f)
updater, creator := anonString, anonString
if folder.CreatedBy > 0 { return dtos.Folder{
creator = hs.getUserLogin(c.Req.Context(), folder.CreatedBy) Id: f.ID,
} Uid: f.UID,
if folder.UpdatedBy > 0 { Title: f.Title,
updater = hs.getUserLogin(c.Req.Context(), folder.UpdatedBy) Url: f.URL,
HasACL: f.HasACL,
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CanDelete: canDelete,
CreatedBy: creator,
Created: f.Created,
UpdatedBy: updater,
Updated: f.Updated,
Version: f.Version,
AccessControl: acMetadata,
ParentUID: f.ParentUID,
}
} }
acMetadata, _ := hs.getFolderACMetadata(c, folder) folderDTO := toDTO(f)
return dtos.Folder{ if !hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) {
Id: folder.ID, return folderDTO
Uid: folder.UID,
Title: folder.Title,
Url: folder.URL,
HasACL: folder.HasACL,
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CanDelete: canDelete,
CreatedBy: creator,
Created: folder.Created,
UpdatedBy: updater,
Updated: folder.Updated,
Version: folder.Version,
AccessControl: acMetadata,
ParentUID: folder.ParentUID,
} }
parents, err := hs.folderService.GetParents(ctx, folder.GetParentsQuery{UID: f.UID, OrgID: f.OrgID})
if err != nil {
// log the error instead of failing
hs.log.Error("failed to fetch folder parents", "folder", f.UID, "org", f.OrgID, "error", err)
}
folderDTO.Parents = make([]dtos.Folder, 0, len(parents))
for _, f := range parents {
folderDTO.Parents = append(folderDTO.Parents, toDTO(f))
}
return folderDTO
} }
func (hs *HTTPServer) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.Folder) (accesscontrol.Metadata, error) { func (hs *HTTPServer) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.Folder) (accesscontrol.Metadata, error) {

View File

@ -422,3 +422,89 @@ func TestFolderMoveAPIEndpoint(t *testing.T) {
}) })
} }
} }
func TestFolderGetAPIEndpoint(t *testing.T) {
folderService := &foldertest.FakeService{
ExpectedFolder: &folder.Folder{
ID: 1,
UID: "uid",
Title: "uid title",
},
ExpectedFolders: []*folder.Folder{
{
UID: "parent",
Title: "parent title",
},
{
UID: "subfolder",
Title: "subfolder title",
},
},
}
setUpRBACGuardian(t)
type testCase struct {
description string
URL string
features *featuremgmt.FeatureManager
expectedCode int
expectedParentUIDs []string
expectedParentTitles []string
permissions []accesscontrol.Permission
}
tcs := []testCase{
{
description: "get folder by UID should return parent folders if nested folder are enabled",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{"parent", "subfolder"},
expectedParentTitles: []string{"parent title", "subfolder title"},
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
},
},
{
description: "get folder by UID should not return parent folders if nested folder are disabled",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(),
expectedParentUIDs: []string{},
expectedParentTitles: []string{},
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
},
},
}
for _, tc := range tcs {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = &setting.Cfg{
RBACEnabled: true,
}
hs.Features = tc.features
hs.folderService = folderService
})
t.Run(tc.description, func(t *testing.T) {
req := srv.NewGetRequest(tc.URL)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, tc.permissions))
resp, err := srv.Send(req)
require.NoError(t, err)
require.Equal(t, tc.expectedCode, resp.StatusCode)
folder := dtos.Folder{}
err = json.NewDecoder(resp.Body).Decode(&folder)
require.NoError(t, err)
require.Equal(t, len(folder.Parents), len(tc.expectedParentUIDs))
require.Equal(t, len(folder.Parents), len(tc.expectedParentTitles))
for i := 0; i < len(tc.expectedParentUIDs); i++ {
assert.Equal(t, tc.expectedParentUIDs[i], folder.Parents[i].Uid)
assert.Equal(t, tc.expectedParentTitles[i], folder.Parents[i].Title)
}
require.NoError(t, resp.Body.Close())
})
}
}

View File

@ -48,12 +48,6 @@ func (f *Folder) IsGeneral() bool {
return f.ID == GeneralFolder.ID && f.Title == GeneralFolder.Title return f.ID == GeneralFolder.ID && f.Title == GeneralFolder.Title
} }
type FolderDTO struct {
Folder
Children []FolderDTO
}
// NewFolder tales a title and returns a Folder with the Created and Updated // NewFolder tales a title and returns a Folder with the Created and Updated
// fields set to the current time. // fields set to the current time.
func NewFolder(title string, description string) *Folder { func NewFolder(title string, description string) *Folder {

View File

@ -13565,6 +13565,13 @@
"description": "only used if nested folders are enabled", "description": "only used if nested folders are enabled",
"type": "string" "type": "string"
}, },
"parents": {
"description": "the parent folders starting from the root going down",
"type": "array",
"items": {
"$ref": "#/definitions/Folder"
}
},
"title": { "title": {
"type": "string" "type": "string"
}, },

View File

@ -4631,6 +4631,13 @@
"description": "only used if nested folders are enabled", "description": "only used if nested folders are enabled",
"type": "string" "type": "string"
}, },
"parents": {
"description": "the parent folders starting from the root going down",
"items": {
"$ref": "#/components/schemas/Folder"
},
"type": "array"
},
"title": { "title": {
"type": "string" "type": "string"
}, },