Nested folders: Refactor folder update (#60323)

* Nested folders: Refactor folder update

* Apply suggestions from code review
This commit is contained in:
Sofia Papagiannaki 2022-12-20 15:00:33 +02:00 committed by GitHub
parent 5d4e35c3d5
commit 55b014974d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 267 additions and 148 deletions

View File

@ -201,11 +201,15 @@ func (hs *HTTPServer) MoveFolder(c *models.ReqContext) response.Response {
// 409: conflictError
// 500: internalServerError
func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
cmd := models.UpdateFolderCommand{}
cmd := folder.UpdateFolderCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
result, err := hs.folderService.Update(c.Req.Context(), c.SignedInUser, c.OrgID, web.Params(c.Req)[":uid"], &cmd)
cmd.OrgID = c.OrgID
cmd.UID = web.Params(c.Req)[":uid"]
cmd.SignedInUser = c.SignedInUser
result, err := hs.folderService.Update(c.Req.Context(), &cmd)
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
@ -320,7 +324,7 @@ type UpdateFolderParams struct {
//
// in:body
// required:true
Body models.UpdateFolderCommand `json:"body"`
Body folder.UpdateFolderCommand `json:"body"`
}
// swagger:parameters getFolderByID

View File

@ -90,8 +90,9 @@ func TestFoldersAPIEndpoint(t *testing.T) {
})
t.Run("Given a correct request for updating a folder", func(t *testing.T) {
cmd := models.UpdateFolderCommand{
Title: "Folder upd",
title := "Folder upd"
cmd := folder.UpdateFolderCommand{
NewTitle: &title,
}
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder upd"}
@ -125,8 +126,9 @@ func TestFoldersAPIEndpoint(t *testing.T) {
{Error: dashboards.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500},
}
cmd := models.UpdateFolderCommand{
Title: "Folder upd",
title := "Folder upd"
cmd := folder.UpdateFolderCommand{
NewTitle: &title,
}
for _, tc := range testCases {
@ -278,7 +280,7 @@ func callUpdateFolder(sc *scenarioContext) {
}
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService folder.Service,
cmd models.UpdateFolderCommand, fn scenarioFunc) {
cmd folder.UpdateFolderCommand, fn scenarioFunc) {
setUpRBACGuardian(t)
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := HTTPServer{

View File

@ -39,7 +39,7 @@ func main() {
// static list of planned core kinds so that we can inject ones that
// haven't been started on yet as "planned"
var plannedCoreKinds = []string {
var plannedCoreKinds = []string{
"Dashboard",
"Playlist",
"Team",
@ -88,11 +88,11 @@ func buildKindStateReport() KindStateReport {
}
r.Core = append(r.Core, kindsys.CoreStructuredProperties{
CommonProperties: kindsys.CommonProperties{
Name: kn,
PluralName: kn + "s",
MachineName: machinize(kn),
Name: kn,
PluralName: kn + "s",
MachineName: machinize(kn),
PluralMachineName: machinize(kn) + "s",
Maturity: "planned",
Maturity: "planned",
},
})
}

View File

@ -1,7 +1,6 @@
package models
import (
"strings"
"time"
"github.com/grafana/grafana/pkg/services/user"
@ -47,27 +46,6 @@ func DashboardToFolder(dash *Dashboard) *Folder {
}
}
// UpdateDashboardModel updates an existing model from command into model for update
func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard, orgId int64, userId int64) {
dashFolder.OrgId = orgId
dashFolder.Title = strings.TrimSpace(cmd.Title)
dashFolder.Data.Set("title", dashFolder.Title)
if cmd.Uid != "" {
dashFolder.SetUid(cmd.Uid)
}
dashFolder.SetVersion(cmd.Version)
dashFolder.IsFolder = true
if userId == 0 {
userId = -1
}
dashFolder.UpdatedBy = userId
dashFolder.UpdateSlug()
}
//
// COMMANDS
//
@ -83,16 +61,6 @@ type MoveFolderCommand struct {
ParentUID *string `json:"parentUid"`
}
type UpdateFolderCommand struct {
Uid string `json:"uid"`
Title string `json:"title"`
Version int `json:"version"`
Description string `json:"description"`
Overwrite bool `json:"overwrite"`
Result *Folder `json:"-"`
}
//
// QUERIES
//

View File

@ -366,33 +366,33 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (
return f, nil
}
func (s *Service) Update(ctx context.Context, user *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) (*folder.Folder, error) {
foldr, err := s.legacyUpdate(ctx, user, orgID, existingUid, cmd)
func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
if cmd.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
user := cmd.SignedInUser
foldr, err := s.legacyUpdate(ctx, cmd)
if err != nil {
return nil, err
}
if s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
if cmd.Uid != "" {
if !util.IsValidShortUID(cmd.Uid) {
if cmd.NewUID != nil && *cmd.NewUID != "" {
if !util.IsValidShortUID(*cmd.NewUID) {
return nil, dashboards.ErrDashboardInvalidUid
} else if util.IsShortUIDTooLong(cmd.Uid) {
} else if util.IsShortUIDTooLong(*cmd.NewUID) {
return nil, dashboards.ErrDashboardUidTooLong
}
}
getFolder, err := s.store.Get(ctx, folder.GetFolderQuery{
UID: &existingUid,
OrgID: orgID,
})
if err != nil {
return nil, err
}
foldr, err := s.store.Update(ctx, folder.UpdateFolderCommand{
Folder: getFolder,
NewUID: &cmd.Uid,
NewTitle: &cmd.Title,
NewDescription: &cmd.Description,
UID: cmd.UID,
OrgID: cmd.OrgID,
NewUID: cmd.NewUID,
NewTitle: cmd.NewTitle,
NewDescription: cmd.NewDescription,
SignedInUser: user,
})
if err != nil {
return nil, err
@ -402,10 +402,10 @@ func (s *Service) Update(ctx context.Context, user *user.SignedInUser, orgID int
return foldr, nil
}
func (s *Service) legacyUpdate(ctx context.Context, user *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) (*folder.Folder, error) {
func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
logger := s.log.FromContext(ctx)
query := models.GetDashboardQuery{OrgId: orgID, Uid: existingUid}
query := models.GetDashboardQuery{OrgId: cmd.OrgID, Uid: cmd.UID}
_, err := s.dashboardStore.GetDashboard(ctx, &query)
if err != nil {
return nil, toFolderError(err)
@ -418,12 +418,17 @@ func (s *Service) legacyUpdate(ctx context.Context, user *user.SignedInUser, org
return nil, dashboards.ErrFolderNotFound
}
cmd.UpdateDashboardModel(dashFolder, orgID, user.UserID)
if cmd.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
user := cmd.SignedInUser
prepareForUpdate(dashFolder, cmd.OrgID, cmd.SignedInUser.UserID, cmd)
dto := &dashboards.SaveDashboardDTO{
Dashboard: dashFolder,
OrgId: orgID,
User: user,
OrgId: cmd.OrgID,
User: cmd.SignedInUser,
Overwrite: cmd.Overwrite,
}
@ -438,7 +443,7 @@ func (s *Service) legacyUpdate(ctx context.Context, user *user.SignedInUser, org
}
var foldr *folder.Folder
foldr, err = s.dashboardStore.GetFolderByID(ctx, orgID, dash.Id)
foldr, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.Id)
if err != nil {
return nil, err
}
@ -449,7 +454,7 @@ func (s *Service) legacyUpdate(ctx context.Context, user *user.SignedInUser, org
Title: foldr.Title,
ID: dash.Id,
UID: dash.Uid,
OrgID: orgID,
OrgID: cmd.OrgID,
}); err != nil {
logger.Error("failed to publish FolderTitleUpdated event", "folder", foldr.Title, "user", user.UserID, "error", err)
}
@ -457,6 +462,32 @@ func (s *Service) legacyUpdate(ctx context.Context, user *user.SignedInUser, org
return foldr, nil
}
// prepareForUpdate updates an existing dashboard model from command into model for folder update
func prepareForUpdate(dashFolder *models.Dashboard, orgId int64, userId int64, cmd *folder.UpdateFolderCommand) {
dashFolder.OrgId = orgId
title := dashFolder.Title
if cmd.NewTitle != nil && *cmd.NewTitle != "" {
title = *cmd.NewTitle
}
dashFolder.Title = strings.TrimSpace(title)
dashFolder.Data.Set("title", dashFolder.Title)
if cmd.NewUID != nil && *cmd.NewUID != "" {
dashFolder.SetUid(*cmd.NewUID)
}
dashFolder.SetVersion(cmd.Version)
dashFolder.IsFolder = true
if userId == 0 {
userId = -1
}
dashFolder.UpdatedBy = userId
dashFolder.UpdateSlug()
}
func (s *Service) DeleteFolder(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
logger := s.log.FromContext(ctx)
if cmd.SignedInUser == nil {
@ -501,23 +532,20 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
foldr, err := s.Get(ctx, &folder.GetFolderQuery{
UID: &cmd.UID,
OrgID: cmd.OrgID,
SignedInUser: cmd.SignedInUser,
})
g, err := guardian.NewByUID(ctx, cmd.UID, cmd.OrgID, cmd.SignedInUser)
if err != nil {
return nil, err
}
// if the new parent is the same as the current parent, we don't need to do anything
if foldr.ParentUID == cmd.NewParentUID {
return foldr, nil
if canSave, err := g.CanSave(); err != nil || !canSave {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
// here we get the folder, we need to get the height of current folder
// and the depth of the new parent folder, the sum can't bypass 8
folderHeight, err := s.store.GetHeight(ctx, foldr.UID, cmd.OrgID, &cmd.NewParentUID)
folderHeight, err := s.store.GetHeight(ctx, cmd.UID, cmd.OrgID, &cmd.NewParentUID)
if err != nil {
return nil, err
}
@ -533,13 +561,16 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol
// if the current folder is already a parent of newparent, we should return error
for _, parent := range parents {
if parent.UID == foldr.UID {
if parent.UID == cmd.UID {
return nil, folder.ErrCircularReference
}
}
return s.store.Update(ctx, folder.UpdateFolderCommand{
Folder: foldr,
UID: cmd.UID,
OrgID: cmd.OrgID,
NewParentUID: &cmd.NewParentUID,
SignedInUser: cmd.SignedInUser,
})
}

View File

@ -10,7 +10,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/xorcare/pointer"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
@ -96,9 +95,10 @@ func TestIntegrationFolderService(t *testing.T) {
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
var zeroInt int64 = 0
t.Run("When get folder by id, with id = 0 should return default folder", func(t *testing.T) {
foldr, err := service.Get(context.Background(), &folder.GetFolderQuery{
ID: pointer.Int64(0),
ID: &zeroInt,
OrgID: orgID,
SignedInUser: usr,
})
@ -126,15 +126,18 @@ func TestIntegrationFolderService(t *testing.T) {
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
title := "Folder-TEST"
t.Run("When updating folder should return access denied error", func(t *testing.T) {
dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
folder := args.Get(1).(*models.GetDashboardQuery)
folder.Result = models.NewDashboard("dashboard-test")
folder.Result.IsFolder = true
}).Return(&models.Dashboard{}, nil)
_, err := service.Update(context.Background(), usr, orgID, folderUID, &models.UpdateFolderCommand{
Uid: folderUID,
Title: "Folder-TEST",
_, err := service.Update(context.Background(), &folder.UpdateFolderCommand{
UID: folderUID,
OrgID: orgID,
NewTitle: &title,
SignedInUser: usr,
})
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
@ -207,12 +210,15 @@ func TestIntegrationFolderService(t *testing.T) {
dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(dashboardFolder, nil)
dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.Id).Return(f, nil)
req := &models.UpdateFolderCommand{
Uid: dashboardFolder.Uid,
Title: "TEST-Folder",
title := "TEST-Folder"
req := &folder.UpdateFolderCommand{
UID: dashboardFolder.Uid,
OrgID: orgID,
NewTitle: &title,
SignedInUser: usr,
}
reqResult, err := service.Update(context.Background(), usr, orgID, dashboardFolder.Uid, req)
reqResult, err := service.Update(context.Background(), req)
require.NoError(t, err)
require.Equal(t, f, reqResult)
})
@ -393,7 +399,7 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) {
func TestNestedFolderService(t *testing.T) {
t.Run("with feature flag unset", func(t *testing.T) {
store := &FakeStore{}
store := NewFakeStore()
dashStore := dashboards.FakeDashboardStore{}
dashboardsvc := dashboards.FakeDashboardService{}
// nothing enabled yet
@ -449,7 +455,7 @@ func TestNestedFolderService(t *testing.T) {
})
t.Run("with nested folder feature flag on", func(t *testing.T) {
store := &FakeStore{}
store := NewFakeStore()
dashStore := &dashboards.FakeDashboardStore{}
dashboardsvc := &dashboards.FakeDashboardService{}
// nothing enabled yet
@ -583,6 +589,34 @@ func TestNestedFolderService(t *testing.T) {
})
})
t.Run("move, no view permission should fail", func(t *testing.T) {
// This test creates and deletes the dashboard, so needs some extra setup.
g := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: false})
t.Cleanup(func() {
guardian.New = g
})
store.ExpectedError = nil
store.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"}
_, err := foldersvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr})
require.Error(t, err, dashboards.ErrFolderAccessDenied)
})
t.Run("move, no save permission should fail", func(t *testing.T) {
// This test creates and deletes the dashboard, so needs some extra setup.
g := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: false, CanViewValue: true})
t.Cleanup(func() {
guardian.New = g
})
store.ExpectedError = nil
store.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"}
_, err := foldersvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr})
require.Error(t, err, dashboards.ErrFolderAccessDenied)
})
t.Run("move, no error", func(t *testing.T) {
// This test creates and deletes the dashboard, so needs some extra setup.
g := guardian.New
@ -593,6 +627,11 @@ func TestNestedFolderService(t *testing.T) {
store.ExpectedError = nil
store.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"}
store.ExpectedParentFolders = []*folder.Folder{
{UID: "newFolder", ParentUID: "newFolder"},
{UID: "newFolder2", ParentUID: "newFolder2"},
{UID: "newFolder3", ParentUID: "newFolder3"},
}
f, err := foldersvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr})
require.NoError(t, err)
require.NotNil(t, f)

View File

@ -91,33 +91,34 @@ func (ss *sqlStore) Delete(ctx context.Context, uid string, orgID int64) error {
}
func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) {
if cmd.Folder == nil {
return nil, folder.ErrBadRequest.Errorf("invalid update command: missing folder")
}
updated := time.Now()
uid := cmd.UID
cmd.Folder.Updated = time.Now()
existingUID := cmd.Folder.UID
var foldr *folder.Folder
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
sql := strings.Builder{}
sql.Write([]byte("UPDATE folder SET "))
columnsToUpdate := []string{"updated = ?"}
args := []interface{}{cmd.Folder.Updated}
args := []interface{}{updated}
if cmd.NewDescription != nil {
columnsToUpdate = append(columnsToUpdate, "description = ?")
cmd.Folder.Description = *cmd.NewDescription
args = append(args, cmd.Folder.Description)
args = append(args, *cmd.NewDescription)
}
if cmd.NewTitle != nil {
columnsToUpdate = append(columnsToUpdate, "title = ?")
cmd.Folder.Title = *cmd.NewTitle
args = append(args, cmd.Folder.Title)
args = append(args, *cmd.NewTitle)
}
if cmd.NewUID != nil {
columnsToUpdate = append(columnsToUpdate, "uid = ?")
cmd.Folder.UID = *cmd.NewUID
args = append(args, cmd.Folder.UID)
uid = *cmd.NewUID
args = append(args, *cmd.NewUID)
}
if cmd.NewParentUID != nil {
columnsToUpdate = append(columnsToUpdate, "parent_uid = ?")
args = append(args, *cmd.NewParentUID)
}
if len(columnsToUpdate) == 0 {
@ -126,7 +127,7 @@ func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand)
sql.Write([]byte(strings.Join(columnsToUpdate, ", ")))
sql.Write([]byte(" WHERE uid = ? AND org_id = ?"))
args = append(args, existingUID, cmd.Folder.OrgID)
args = append(args, cmd.UID, cmd.OrgID)
args = append([]interface{}{sql.String()}, args...)
@ -142,10 +143,18 @@ func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand)
if affected == 0 {
return folder.ErrInternal.Errorf("no folders are updated")
}
foldr, err = ss.Get(ctx, folder.GetFolderQuery{
UID: &uid,
OrgID: cmd.OrgID,
})
if err != nil {
return err
}
return nil
})
return cmd.Folder, err
return foldr, err
}
func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) {

View File

@ -9,7 +9,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
@ -156,7 +155,7 @@ func TestIntegrationDelete(t *testing.T) {
})
*/
ancestorUIDs := CreateSubTree(t, folderStore, orgID, accesscontrol.GeneralFolderUID, folder.MaxNestedFolderDepth, "")
ancestorUIDs := CreateSubTree(t, folderStore, orgID, "", folder.MaxNestedFolderDepth, "")
require.Len(t, ancestorUIDs, folder.MaxNestedFolderDepth)
t.Cleanup(func() {
@ -235,9 +234,7 @@ func TestIntegrationUpdate(t *testing.T) {
_, err = folderStore.Update(context.Background(), folder.UpdateFolderCommand{})
require.Error(t, err)
_, err = folderStore.Update(context.Background(), folder.UpdateFolderCommand{
Folder: &folder.Folder{},
})
_, err = folderStore.Update(context.Background(), folder.UpdateFolderCommand{})
require.Error(t, err)
})
@ -246,7 +243,8 @@ func TestIntegrationUpdate(t *testing.T) {
newDesc := "new desc"
// existingUpdated := f.Updated
updated, err := folderStore.Update(context.Background(), folder.UpdateFolderCommand{
Folder: f,
UID: f.UID,
OrgID: f.OrgID,
NewTitle: &newTitle,
NewDescription: &newDesc,
})
@ -264,6 +262,8 @@ func TestIntegrationUpdate(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, newTitle, updated.Title)
assert.Equal(t, newDesc, updated.Description)
f = updated
})
t.Run("updating folder UID should succeed", func(t *testing.T) {
@ -271,7 +271,8 @@ func TestIntegrationUpdate(t *testing.T) {
existingTitle := f.Title
existingDesc := f.Description
updated, err := folderStore.Update(context.Background(), folder.UpdateFolderCommand{
Folder: f,
UID: f.UID,
OrgID: f.OrgID,
NewUID: &newUID,
})
require.NoError(t, err)
@ -641,13 +642,16 @@ func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 {
func CreateSubTree(t *testing.T, store *sqlStore, orgID int64, parentUID string, depth int, prefix string) []string {
t.Helper()
ancestorUIDs := []string{parentUID}
ancestorUIDs := []string{}
if parentUID != "" {
ancestorUIDs = append(ancestorUIDs, parentUID)
}
for i := 0; i < depth; i++ {
title := fmt.Sprintf("%sfolder-%d", prefix, i)
cmd := folder.CreateFolderCommand{
Title: title,
OrgID: orgID,
ParentUID: ancestorUIDs[len(ancestorUIDs)-1],
ParentUID: parentUID,
UID: util.GenerateShortUID(),
}
f, err := store.Create(context.Background(), cmd)
@ -668,6 +672,8 @@ func CreateSubTree(t *testing.T, store *sqlStore, orgID int64, parentUID string,
require.Equal(t, ancestorUIDs, parentUIDs)
ancestorUIDs = append(ancestorUIDs, f.UID)
parentUID = f.UID
}
return ancestorUIDs

View File

@ -6,7 +6,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
)
type FakeStore struct {
type fakeStore struct {
ExpectedChildFolders []*folder.Folder
ExpectedParentFolders []*folder.Folder
ExpectedFolder *folder.Folder
@ -16,42 +16,42 @@ type FakeStore struct {
DeleteCalled bool
}
func NewFakeStore() *FakeStore {
return &FakeStore{}
func NewFakeStore() *fakeStore {
return &fakeStore{}
}
var _ store = (*FakeStore)(nil)
var _ store = (*fakeStore)(nil)
func (f *FakeStore) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) {
func (f *fakeStore) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) {
f.CreateCalled = true
return f.ExpectedFolder, f.ExpectedError
}
func (f *FakeStore) Delete(ctx context.Context, uid string, orgID int64) error {
func (f *fakeStore) Delete(ctx context.Context, uid string, orgID int64) error {
f.DeleteCalled = true
return f.ExpectedError
}
func (f *FakeStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) {
func (f *fakeStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) {
return f.ExpectedFolder, f.ExpectedError
}
func (f *FakeStore) Move(ctx context.Context, cmd folder.MoveFolderCommand) error {
func (f *fakeStore) Move(ctx context.Context, cmd folder.MoveFolderCommand) error {
return f.ExpectedError
}
func (f *FakeStore) Get(ctx context.Context, cmd folder.GetFolderQuery) (*folder.Folder, error) {
func (f *fakeStore) Get(ctx context.Context, cmd folder.GetFolderQuery) (*folder.Folder, error) {
return f.ExpectedFolder, f.ExpectedError
}
func (f *FakeStore) GetParents(ctx context.Context, cmd folder.GetParentsQuery) ([]*folder.Folder, error) {
func (f *fakeStore) GetParents(ctx context.Context, cmd folder.GetParentsQuery) ([]*folder.Folder, error) {
return f.ExpectedParentFolders, f.ExpectedError
}
func (f *FakeStore) GetChildren(ctx context.Context, cmd folder.GetChildrenQuery) ([]*folder.Folder, error) {
func (f *fakeStore) GetChildren(ctx context.Context, cmd folder.GetChildrenQuery) ([]*folder.Folder, error) {
return f.ExpectedChildFolders, f.ExpectedError
}
func (f *FakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64, parentUID *string) (int, error) {
func (f *fakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64, parentUID *string) (int, error) {
return f.ExpectedFolderHeight, f.ExpectedError
}

View File

@ -3,9 +3,7 @@ package foldertest
import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/user"
)
type FakeService struct {
@ -29,8 +27,7 @@ func (s *FakeService) Create(ctx context.Context, cmd *folder.CreateFolderComman
func (s *FakeService) Get(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) Update(ctx context.Context, user *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) (*folder.Folder, error) {
cmd.Result = s.ExpectedFolder.ToLegacyModel()
func (s *FakeService) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) DeleteFolder(ctx context.Context, cmd *folder.DeleteFolderCommand) error {

View File

@ -76,10 +76,20 @@ type CreateFolderCommand struct {
// UpdateFolderCommand captures the information required by the folder service
// to update a folder. Use Move to update a folder's parent folder.
type UpdateFolderCommand struct {
Folder *Folder `json:"folder"` // The extant folder
NewUID *string `json:"uid" xorm:"uid"`
NewTitle *string `json:"title"`
NewDescription *string `json:"description"`
UID string `json:"-"`
OrgID int64 `json:"-"`
// NewUID it's an optional parameter used for overriding the existing folder UID
NewUID *string `json:"uid"` // keep same json tag with the legacy command for not breaking the existing APIs
// NewTitle it's an optional parameter used for overriding the existing folder title
NewTitle *string `json:"title"` // keep same json tag with the legacy command for not breaking the existing APIs
// NewDescription it's an optional parameter used for overriding the existing folder description
NewDescription *string `json:"description"` // keep same json tag with the legacy command for not breaking the existing APIs
NewParentUID *string `json:"-"`
// Version only used by the legacy folder implementation
Version int `json:"version"`
// Overwrite only used by the legacy folder implementation
Overwrite bool `json:"overwrite"`
SignedInUser *user.SignedInUser `json:"-"`
}

View File

@ -2,9 +2,6 @@ package folder
import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/user"
)
type Service interface {
@ -20,7 +17,7 @@ type Service interface {
// Update is used to update a folder's UID, Title and Description. To change
// a folder's parent folder, use Move.
Update(ctx context.Context, user *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) (*Folder, error)
Update(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
DeleteFolder(ctx context.Context, cmd *DeleteFolderCommand) error
MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error
// Move changes a folder's parent folder to the requested new parent.

View File

@ -16553,7 +16553,7 @@
}
}
},
"SearchServiceAccountsResult": {
"SearchOrgServiceAccountsResult": {
"description": "swagger: model",
"type": "object",
"properties": {
@ -17864,21 +17864,27 @@
}
},
"UpdateFolderCommand": {
"description": "UpdateFolderCommand captures the information required by the folder service\nto update a folder. Use Move to update a folder's parent folder.",
"type": "object",
"properties": {
"description": {
"description": "NewDescription it's an optional parameter used for overriding the existing folder description",
"type": "string"
},
"overwrite": {
"description": "Overwrite only used by the legacy folder implementation",
"type": "boolean"
},
"title": {
"description": "NewTitle it's an optional parameter used for overriding the existing folder title",
"type": "string"
},
"uid": {
"description": "NewUID it's an optional parameter used for overriding the existing folder UID",
"type": "string"
},
"version": {
"description": "Version only used by the legacy folder implementation",
"type": "integer",
"format": "int64"
}
@ -18049,6 +18055,10 @@
"Editor",
"Admin"
]
},
"serviceAccountId": {
"type": "integer",
"format": "int64"
}
}
},
@ -20106,7 +20116,7 @@
"searchOrgServiceAccountsWithPagingResponse": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/SearchServiceAccountsResult"
"$ref": "#/definitions/SearchOrgServiceAccountsResult"
}
},
"searchOrgsResponse": {

View File

@ -13618,7 +13618,7 @@
}
}
},
"SearchServiceAccountsResult": {
"SearchOrgServiceAccountsResult": {
"description": "swagger: model",
"type": "object",
"properties": {
@ -14493,21 +14493,27 @@
}
},
"UpdateFolderCommand": {
"description": "UpdateFolderCommand captures the information required by the folder service\nto update a folder. Use Move to update a folder's parent folder.",
"type": "object",
"properties": {
"description": {
"description": "NewDescription it's an optional parameter used for overriding the existing folder description",
"type": "string"
},
"overwrite": {
"description": "Overwrite only used by the legacy folder implementation",
"type": "boolean"
},
"title": {
"description": "NewTitle it's an optional parameter used for overriding the existing folder title",
"type": "string"
},
"uid": {
"description": "NewUID it's an optional parameter used for overriding the existing folder UID",
"type": "string"
},
"version": {
"description": "Version only used by the legacy folder implementation",
"type": "integer",
"format": "int64"
}
@ -14678,6 +14684,10 @@
"Editor",
"Admin"
]
},
"serviceAccountId": {
"type": "integer",
"format": "int64"
}
}
},
@ -16047,7 +16057,7 @@
"searchOrgServiceAccountsWithPagingResponse": {
"description": "",
"schema": {
"$ref": "#/definitions/SearchServiceAccountsResult"
"$ref": "#/definitions/SearchOrgServiceAccountsResult"
}
},
"searchOrgsResponse": {

View File

@ -1581,7 +1581,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchServiceAccountsResult"
"$ref": "#/components/schemas/SearchOrgServiceAccountsResult"
}
}
},
@ -6955,6 +6955,12 @@
],
"type": "object"
},
"ProvisionedAlertRules": {
"items": {
"$ref": "#/components/schemas/ProvisionedAlertRule"
},
"type": "array"
},
"PushoverConfig": {
"properties": {
"expire": {
@ -7837,7 +7843,7 @@
},
"type": "object"
},
"SearchServiceAccountsResult": {
"SearchOrgServiceAccountsResult": {
"description": "swagger: model",
"properties": {
"page": {
@ -8914,7 +8920,6 @@
"type": "string"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.",
"properties": {
"ForceQuery": {
"type": "boolean"
@ -8925,6 +8930,9 @@
"Host": {
"type": "string"
},
"OmitHost": {
"type": "boolean"
},
"Opaque": {
"type": "string"
},
@ -8947,7 +8955,7 @@
"$ref": "#/components/schemas/Userinfo"
}
},
"title": "A URL represents a parsed URL (technically, a URI reference).",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"type": "object"
},
"UpdateAlertNotificationCommand": {
@ -9145,20 +9153,26 @@
"type": "object"
},
"UpdateFolderCommand": {
"description": "UpdateFolderCommand captures the information required by the folder service\nto update a folder. Use Move to update a folder's parent folder.",
"properties": {
"description": {
"description": "NewDescription it's an optional parameter used for overriding the existing folder description",
"type": "string"
},
"overwrite": {
"description": "Overwrite only used by the legacy folder implementation",
"type": "boolean"
},
"title": {
"description": "NewTitle it's an optional parameter used for overriding the existing folder title",
"type": "string"
},
"uid": {
"description": "NewUID it's an optional parameter used for overriding the existing folder UID",
"type": "string"
},
"version": {
"description": "Version only used by the legacy folder implementation",
"format": "int64",
"type": "integer"
}
@ -9329,6 +9343,10 @@
"Admin"
],
"type": "string"
},
"serviceAccountId": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
@ -9851,6 +9869,7 @@
"type": "object"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": {
"annotations": {
"$ref": "#/components/schemas/labelSet"
@ -9969,7 +9988,6 @@
"type": "array"
},
"integration": {
"description": "Integration integration",
"properties": {
"lastNotifyAttempt": {
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
@ -10113,7 +10131,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@ -12888,6 +12905,25 @@
}
},
"/api/v1/provisioning/alert-rules": {
"get": {
"operationId": "RouteGetAlertRules",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProvisionedAlertRules"
}
}
},
"description": "ProvisionedAlertRules"
}
},
"summary": "Get all the alert rules.",
"tags": [
"provisioning"
]
},
"post": {
"operationId": "RoutePostAlertRule",
"parameters": [