API: Support creating a nested folder (#58508)
* API: Support nested folder creation * Update swagger * fixup * Update pkg/api/dtos/folder.go Co-authored-by: Serge Zaitsev <serge.zaitsev@grafana.com> * Fix some tests * create legacy folder url from title and uid Co-authored-by: idafurjes <36131195+idafurjes@users.noreply.github.com> Co-authored-by: Serge Zaitsev <serge.zaitsev@grafana.com> Co-authored-by: Ida Furjesova <ida.furjesova@grafana.com>
This commit is contained in:
co-authored by
Serge Zaitsev
idafurjes
Ida Furjesova
parent
b5388bb080
commit
bf5a08e039
@@ -22,6 +22,8 @@ type Folder struct {
|
||||
Updated time.Time `json:"updated"`
|
||||
Version int `json:"version"`
|
||||
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
|
||||
// only used if nested folders are enabled
|
||||
ParentUID string `json:"parentUid"`
|
||||
}
|
||||
|
||||
type FolderSearchHit struct {
|
||||
|
||||
+50
-5
@@ -105,6 +105,8 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response {
|
||||
//
|
||||
// Create folder.
|
||||
//
|
||||
// If nested folders are enabled then it additionally expects the parent folder UID.
|
||||
//
|
||||
// Responses:
|
||||
// 200: folderResponse
|
||||
// 400: badRequestError
|
||||
@@ -113,23 +115,29 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response {
|
||||
// 409: conflictError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
|
||||
cmd := models.CreateFolderCommand{}
|
||||
cmd := folder.CreateFolderCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
folder, err := hs.folderService.CreateFolder(c.Req.Context(), c.SignedInUser, c.OrgID, cmd.Title, cmd.Uid)
|
||||
cmd.OrgID = c.OrgID
|
||||
|
||||
folder, err := hs.folderService.Create(c.Req.Context(), &cmd)
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), folder.Id, c.OrgID, c.SignedInUser)
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c, g, folder))
|
||||
g := guardian.New(c.Req.Context(), folder.ID, c.OrgID, c.SignedInUser)
|
||||
// TODO set ParentUID if nested folders are enabled
|
||||
return response.JSON(http.StatusOK, hs.newToFolderDto(c, g, folder))
|
||||
}
|
||||
|
||||
// swagger:route PUT /folders/{folder_uid} folders updateFolder
|
||||
//
|
||||
// Update folder.
|
||||
//
|
||||
// If nested folders are enabled then it optionally expects a new parent folder UID that moves the folder and
|
||||
// includes it into the response.
|
||||
//
|
||||
// Responses:
|
||||
// 200: folderResponse
|
||||
// 400: badRequestError
|
||||
@@ -156,6 +164,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
|
||||
// Delete folder.
|
||||
//
|
||||
// Deletes an existing folder identified by UID along with all dashboards (and their alerts) stored in the folder. This operation cannot be reverted.
|
||||
// If nested folders are enabled then it also deletes all the subfolders.
|
||||
//
|
||||
// Responses:
|
||||
// 200: deleteFolderResponse
|
||||
@@ -216,6 +225,42 @@ func (hs *HTTPServer) toFolderDto(c *models.ReqContext, g guardian.DashboardGuar
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) newToFolderDto(c *models.ReqContext, g guardian.DashboardGuardian, folder *folder.Folder) dtos.Folder {
|
||||
canEdit, _ := g.CanEdit()
|
||||
canSave, _ := g.CanSave()
|
||||
canAdmin, _ := g.CanAdmin()
|
||||
canDelete, _ := g.CanDelete()
|
||||
|
||||
// Finding creator and last updater of the folder
|
||||
updater, creator := anonString, anonString
|
||||
/*
|
||||
if folder.CreatedBy > 0 {
|
||||
creator = hs.getUserLogin(c.Req.Context(), folder.CreatedBy)
|
||||
}
|
||||
if folder.UpdatedBy > 0 {
|
||||
updater = hs.getUserLogin(c.Req.Context(), folder.UpdatedBy)
|
||||
}
|
||||
*/
|
||||
|
||||
return dtos.Folder{
|
||||
Id: folder.ID,
|
||||
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: hs.getAccessControlMetadata(c, c.OrgID, dashboards.ScopeFoldersPrefix, folder.UID),
|
||||
}
|
||||
}
|
||||
|
||||
// swagger:parameters getFolders
|
||||
type GetFoldersParams struct {
|
||||
// Limit the maximum number of folders to return
|
||||
@@ -262,7 +307,7 @@ type GetFolderByIDParams struct {
|
||||
type CreateFolderParams struct {
|
||||
// in:body
|
||||
// required:true
|
||||
Body models.CreateFolderCommand `json:"body"`
|
||||
Body folder.CreateFolderCommand `json:"body"`
|
||||
}
|
||||
|
||||
// swagger:parameters deleteFolder
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
service "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -130,7 +131,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
folderService.ExpectedFolder = &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"}
|
||||
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
|
||||
@@ -187,7 +188,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
|
||||
})
|
||||
|
||||
folderService.ExpectedFolder = &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"}
|
||||
|
||||
cmd := dtos.UpdateDashboardACLCommand{
|
||||
Items: []dtos.DashboardACLUpdateItem{
|
||||
@@ -251,7 +252,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
|
||||
)
|
||||
|
||||
folderService.ExpectedFolder = &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"}
|
||||
|
||||
cmd := dtos.UpdateDashboardACLCommand{
|
||||
Items: []dtos.DashboardACLUpdateItem{
|
||||
@@ -297,7 +298,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
|
||||
var gotItems []*models.DashboardACL
|
||||
|
||||
folderService.ExpectedFolder = &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"}
|
||||
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
gotItems = args.Get(2).([]*models.DashboardACL)
|
||||
}).Return(nil).Once()
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestFoldersAPIEndpoint(t *testing.T) {
|
||||
Title: "Folder",
|
||||
}
|
||||
|
||||
folderService.ExpectedFolder = &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"}
|
||||
|
||||
createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", folderService, cmd,
|
||||
func(sc *scenarioContext) {
|
||||
@@ -94,7 +94,7 @@ func TestFoldersAPIEndpoint(t *testing.T) {
|
||||
Title: "Folder upd",
|
||||
}
|
||||
|
||||
folderService.ExpectedFolder = &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"}
|
||||
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder upd"}
|
||||
|
||||
updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", folderService, cmd,
|
||||
func(sc *scenarioContext) {
|
||||
@@ -179,7 +179,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should attach access control metadata to folder response", func(t *testing.T) {
|
||||
folderService.ExpectedFolder = &models.Folder{Uid: "folderUid"}
|
||||
folderService.ExpectedFolder = &folder.Folder{UID: "folderUid"}
|
||||
|
||||
req := server.NewGetRequest("/api/folders/folderUid?accesscontrol=true")
|
||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
@@ -202,7 +202,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should attach access control metadata to folder response", func(t *testing.T) {
|
||||
folderService.ExpectedFolder = &models.Folder{Uid: "folderUid"}
|
||||
folderService.ExpectedFolder = &folder.Folder{UID: "folderUid"}
|
||||
|
||||
req := server.NewGetRequest("/api/folders/folderUid")
|
||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
|
||||
Reference in New Issue
Block a user