diff --git a/pkg/services/librarypanels/api.go b/pkg/services/librarypanels/api.go index e4e46b165df..589a4a5fe25 100644 --- a/pkg/services/librarypanels/api.go +++ b/pkg/services/librarypanels/api.go @@ -21,6 +21,7 @@ func (lps *LibraryPanelService) registerAPIEndpoints() { libraryPanels.Delete("/:uid", middleware.ReqSignedIn, api.Wrap(lps.deleteHandler)) libraryPanels.Get("/", middleware.ReqSignedIn, api.Wrap(lps.getAllHandler)) libraryPanels.Get("/:uid", middleware.ReqSignedIn, api.Wrap(lps.getHandler)) + libraryPanels.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryPanelCommand{}), api.Wrap(lps.patchHandler)) }) } @@ -28,8 +29,8 @@ func (lps *LibraryPanelService) registerAPIEndpoints() { func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd createLibraryPanelCommand) api.Response { panel, err := lps.createLibraryPanel(c, cmd) if err != nil { - if errors.Is(err, errLibraryPanelAlreadyAdded) { - return api.Error(400, errLibraryPanelAlreadyAdded.Error(), err) + if errors.Is(err, errLibraryPanelAlreadyExists) { + return api.Error(400, errLibraryPanelAlreadyExists.Error(), err) } return api.Error(500, "Failed to create library panel", err) } @@ -72,3 +73,19 @@ func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) api.Response return api.JSON(200, util.DynMap{"result": libraryPanels}) } + +// patchHandler handles PATCH /api/library-panels/:uid +func (lps *LibraryPanelService) patchHandler(c *models.ReqContext, cmd patchLibraryPanelCommand) api.Response { + libraryPanel, err := lps.patchLibraryPanel(c, cmd, c.Params(":uid")) + if err != nil { + if errors.Is(err, errLibraryPanelAlreadyExists) { + return api.Error(400, errLibraryPanelAlreadyExists.Error(), err) + } + if errors.Is(err, errLibraryPanelNotFound) { + return api.Error(404, errLibraryPanelNotFound.Error(), err) + } + return api.Error(500, "Failed to update library panel", err) + } + + return api.JSON(200, util.DynMap{"result": libraryPanel}) +} diff --git a/pkg/services/librarypanels/database.go b/pkg/services/librarypanels/database.go index 8c1177dd337..27a2213ab81 100644 --- a/pkg/services/librarypanels/database.go +++ b/pkg/services/librarypanels/database.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" ) -// createLibraryPanel adds a Library Panel +// createLibraryPanel adds a Library Panel. func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd createLibraryPanelCommand) (LibraryPanel, error) { libraryPanel := LibraryPanel{ OrgID: c.SignedInUser.OrgId, @@ -28,14 +28,10 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre UpdatedBy: c.SignedInUser.UserId, } err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { - if res, err := session.Query("SELECT 1 FROM library_panel WHERE org_id=? AND folder_id=? AND name=?", - c.SignedInUser.OrgId, cmd.FolderID, cmd.Name); err != nil { - return err - } else if len(res) == 1 { - return errLibraryPanelAlreadyAdded - } - if _, err := session.Insert(&libraryPanel); err != nil { + if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return errLibraryPanelAlreadyExists + } return err } return nil @@ -44,7 +40,7 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre return libraryPanel, err } -// deleteLibraryPanel deletes a Library Panel +// deleteLibraryPanel deletes a Library Panel. func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid string) error { orgID := c.SignedInUser.OrgId return lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { @@ -63,26 +59,31 @@ func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid str }) } +func getLibraryPanel(session *sqlstore.DBSession, uid string, orgID int64) (LibraryPanel, error) { + libraryPanels := make([]LibraryPanel, 0) + session.Table("library_panel") + session.Where("uid=? AND org_id=?", uid, orgID) + err := session.Find(&libraryPanels) + if err != nil { + return LibraryPanel{}, err + } + if len(libraryPanels) == 0 { + return LibraryPanel{}, errLibraryPanelNotFound + } + if len(libraryPanels) > 1 { + return LibraryPanel{}, fmt.Errorf("found %d panels, while expecting at most one", len(libraryPanels)) + } + + return libraryPanels[0], nil +} + // getLibraryPanel gets a Library Panel. func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string) (LibraryPanel, error) { - orgID := c.SignedInUser.OrgId var libraryPanel LibraryPanel err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error { - libraryPanels := make([]LibraryPanel, 0) - err := session.SQL("SELECT * FROM library_panel WHERE uid=? and org_id=?", uid, orgID).Find(&libraryPanels) - if err != nil { - return err - } - if len(libraryPanels) == 0 { - return errLibraryPanelNotFound - } - if len(libraryPanels) > 1 { - return fmt.Errorf("found %d panels, while expecting at most one", len(libraryPanels)) - } - - libraryPanel = libraryPanels[0] - - return nil + var err error + libraryPanel, err = getLibraryPanel(session, uid, c.SignedInUser.OrgId) + return err }) return libraryPanel, err @@ -92,7 +93,6 @@ func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext) ([]LibraryPanel, error) { orgID := c.SignedInUser.OrgId libraryPanels := make([]LibraryPanel, 0) - err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error { err := session.SQL("SELECT * FROM library_panel WHERE org_id=?", orgID).Find(&libraryPanels) if err != nil { @@ -104,3 +104,50 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext) ([]Lib return libraryPanels, err } + +// patchLibraryPanel updates a Library Panel. +func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patchLibraryPanelCommand, uid string) (LibraryPanel, error) { + var libraryPanel LibraryPanel + err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { + panelInDB, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) + if err != nil { + return err + } + + libraryPanel = LibraryPanel{ + ID: panelInDB.ID, + OrgID: c.SignedInUser.OrgId, + FolderID: cmd.FolderID, + UID: uid, + Name: cmd.Name, + Model: cmd.Model, + Created: panelInDB.Created, + CreatedBy: panelInDB.CreatedBy, + Updated: time.Now(), + UpdatedBy: c.SignedInUser.UserId, + } + + if cmd.FolderID == 0 { + libraryPanel.FolderID = panelInDB.FolderID + } + if cmd.Name == "" { + libraryPanel.Name = panelInDB.Name + } + if cmd.Model == nil { + libraryPanel.Model = panelInDB.Model + } + + if rowsAffected, err := session.ID(panelInDB.ID).Update(&libraryPanel); err != nil { + if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return errLibraryPanelAlreadyExists + } + return err + } else if rowsAffected != 1 { + return errLibraryPanelNotFound + } + + return nil + }) + + return libraryPanel, err +} diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index d933a6a4618..cd708a6ad49 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -60,7 +60,11 @@ func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) { {Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, {Name: "updated_by", Type: migrator.DB_BigInt, Nullable: false}, }, + Indices: []*migrator.Index{ + {Cols: []string{"org_id", "folder_id", "name"}, Type: migrator.UniqueIndex}, + }, } mg.AddMigration("create library_panel table v1", migrator.NewAddTableMigration(libraryPanelV1)) + mg.AddMigration("add index library_panel org_id & folder_id & name", migrator.NewAddIndexMigration(libraryPanelV1, libraryPanelV1.Indices[0])) } diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index c02ce801656..8b6d0372782 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "gopkg.in/macaron.v1" - + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" @@ -178,12 +178,229 @@ func TestGetAllLibraryPanels(t *testing.T) { }) } +func TestPatchLibraryPanel(t *testing.T) { + testScenario(t, "When an admin tries to patch a library panel that does not exist, it should fail", + func(t *testing.T, sc scenarioContext) { + cmd := patchLibraryPanelCommand{} + sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"}) + response := sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 404, response.Status()) + }) + + testScenario(t, "When an admin tries to patch a library panel that exists, it should succeed", + func(t *testing.T, sc scenarioContext) { + command := getCreateCommand(1, "Text - Library Panel") + response := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + var existing libraryPanelResult + err := json.Unmarshal(response.Body(), &existing) + require.NoError(t, err) + + cmd := patchLibraryPanelCommand{ + FolderID: 2, + Name: "Panel - New name", + Model: []byte(` + { + "datasource": "${DS_GDEV-TESTDATA}", + "id": 1, + "name": "Model - New name", + "type": "text" + } + `), + } + sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID}) + response = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 200, response.Status()) + + var result libraryPanelResult + err = json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + existing.Result.FolderID = int64(2) + existing.Result.Name = "Panel - New name" + existing.Result.Model["name"] = "Model - New name" + if diff := cmp.Diff(existing.Result, result.Result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + testScenario(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result", + func(t *testing.T, sc scenarioContext) { + command := getCreateCommand(1, "Text - Library Panel") + response := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + var existing libraryPanelResult + err := json.Unmarshal(response.Body(), &existing) + require.NoError(t, err) + + cmd := patchLibraryPanelCommand{ + FolderID: 100, + } + sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID}) + response = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 200, response.Status()) + + var result libraryPanelResult + err = json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + existing.Result.FolderID = int64(100) + if diff := cmp.Diff(existing.Result, result.Result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + testScenario(t, "When an admin tries to patch a library panel with name only, it should change name successfully and return correct result", + func(t *testing.T, sc scenarioContext) { + command := getCreateCommand(1, "Text - Library Panel") + response := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + var existing libraryPanelResult + err := json.Unmarshal(response.Body(), &existing) + require.NoError(t, err) + + cmd := patchLibraryPanelCommand{ + Name: "New Name", + } + sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID}) + response = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 200, response.Status()) + var result libraryPanelResult + err = json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + existing.Result.Name = "New Name" + if diff := cmp.Diff(existing.Result, result.Result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + testScenario(t, "When an admin tries to patch a library panel with model only, it should change model successfully and return correct result", + func(t *testing.T, sc scenarioContext) { + command := getCreateCommand(1, "Text - Library Panel") + response := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + var existing libraryPanelResult + err := json.Unmarshal(response.Body(), &existing) + require.NoError(t, err) + + cmd := patchLibraryPanelCommand{ + Model: []byte(`{ "name": "New Model Name" }`), + } + sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID}) + response = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 200, response.Status()) + var result libraryPanelResult + err = json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + existing.Result.Model = map[string]interface{}{ + "name": "New Model Name", + } + if diff := cmp.Diff(existing.Result, result.Result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + testScenario(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result", + func(t *testing.T, sc scenarioContext) { + command := getCreateCommand(1, "Text - Library Panel") + response := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + var existing libraryPanelResult + err := json.Unmarshal(response.Body(), &existing) + require.NoError(t, err) + + cmd := patchLibraryPanelCommand{} + sc.reqContext.UserId = 2 + sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID}) + response = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 200, response.Status()) + var result libraryPanelResult + err = json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + existing.Result.UpdatedBy = int64(2) + if diff := cmp.Diff(existing.Result, result.Result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + testScenario(t, "When an admin tries to patch a library panel with a name that already exists, it should fail", + func(t *testing.T, sc scenarioContext) { + command := getCreateCommand(1, "Existing") + response := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + command = getCreateCommand(1, "Text - Library Panel") + response = sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + var result libraryPanelResult + err := json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + + cmd := patchLibraryPanelCommand{ + Name: "Existing", + } + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) + response = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 400, response.Status()) + }) + + testScenario(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail", + func(t *testing.T, sc scenarioContext) { + command := getCreateCommand(2, "Text - Library Panel") + response := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + command = getCreateCommand(1, "Text - Library Panel") + response = sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + var result libraryPanelResult + err := json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + + cmd := patchLibraryPanelCommand{ + FolderID: 2, + } + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) + response = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 400, response.Status()) + }) + + testScenario(t, "When an admin tries to patch a library panel in another org, it should fail", + func(t *testing.T, sc scenarioContext) { + command := getCreateCommand(1, "Text - Library Panel") + response := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, response.Status()) + + var result libraryPanelResult + err := json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + + cmd := patchLibraryPanelCommand{ + FolderID: 2, + } + sc.reqContext.OrgId = 2 + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) + response = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 404, response.Status()) + }) +} + type libraryPanel struct { - ID int64 `json:"id"` - OrgID int64 `json:"orgId"` - FolderID int64 `json:"folderId"` - UID string `json:"uid"` - Name string `json:"name"` + ID int64 `json:"id"` + OrgID int64 `json:"orgId"` + FolderID int64 `json:"folderId"` + UID string `json:"uid"` + Name string `json:"name"` + Model map[string]interface{} `json:"model"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + CreatedBy int64 `json:"createdBy"` + UpdatedBy int64 `json:"updatedBy"` } type libraryPanelResult struct { @@ -280,3 +497,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo fn(t, sc) }) } + +func getCompareOptions() []cmp.Option { + return []cmp.Option{ + cmp.Transformer("Time", func(in time.Time) int64 { + return in.UTC().Unix() + }), + } +} diff --git a/pkg/services/librarypanels/models.go b/pkg/services/librarypanels/models.go index 711ba4da252..bc384cb8229 100644 --- a/pkg/services/librarypanels/models.go +++ b/pkg/services/librarypanels/models.go @@ -23,8 +23,8 @@ type LibraryPanel struct { } var ( - // errLibraryPanelAlreadyAdded is an error for when the user tries to add a library panel that already exists. - errLibraryPanelAlreadyAdded = errors.New("library panel with that name already exists") + // errLibraryPanelAlreadyExists is an error for when the user tries to add a library panel that already exists. + errLibraryPanelAlreadyExists = errors.New("library panel with that name already exists") // errLibraryPanelNotFound is an error for when a library panel can't be found. errLibraryPanelNotFound = errors.New("library panel could not be found") ) @@ -37,3 +37,10 @@ type createLibraryPanelCommand struct { Name string `json:"name"` Model json.RawMessage `json:"model"` } + +// patchLibraryPanelCommand is the command for patching a LibraryPanel +type patchLibraryPanelCommand struct { + FolderID int64 `json:"folderId"` + Name string `json:"name"` + Model json.RawMessage `json:"model"` +}