PanelLibrary: Add PATCH to the API (#29956)

* PanelLibrary: Adds uid and renames title to name

* Chore: removing lines

* PanelLibrary: Adds update to api

* Update pkg/services/librarypanels/librarypanels_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/librarypanels/database.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/librarypanels/librarypanels_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/librarypanels/librarypanels_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/librarypanels/librarypanels_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Chore: changes after PR comments

* Replace references to put with patch

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Hugo Häggmark 2020-12-23 12:42:52 +01:00 committed by GitHub
parent e375fb0f2b
commit 40d17af2e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 337 additions and 37 deletions

View File

@ -21,6 +21,7 @@ func (lps *LibraryPanelService) registerAPIEndpoints() {
libraryPanels.Delete("/:uid", middleware.ReqSignedIn, api.Wrap(lps.deleteHandler)) libraryPanels.Delete("/:uid", middleware.ReqSignedIn, api.Wrap(lps.deleteHandler))
libraryPanels.Get("/", middleware.ReqSignedIn, api.Wrap(lps.getAllHandler)) libraryPanels.Get("/", middleware.ReqSignedIn, api.Wrap(lps.getAllHandler))
libraryPanels.Get("/:uid", middleware.ReqSignedIn, api.Wrap(lps.getHandler)) 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 { func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd createLibraryPanelCommand) api.Response {
panel, err := lps.createLibraryPanel(c, cmd) panel, err := lps.createLibraryPanel(c, cmd)
if err != nil { if err != nil {
if errors.Is(err, errLibraryPanelAlreadyAdded) { if errors.Is(err, errLibraryPanelAlreadyExists) {
return api.Error(400, errLibraryPanelAlreadyAdded.Error(), err) return api.Error(400, errLibraryPanelAlreadyExists.Error(), err)
} }
return api.Error(500, "Failed to create library panel", 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}) 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})
}

View File

@ -12,7 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore" "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) { func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd createLibraryPanelCommand) (LibraryPanel, error) {
libraryPanel := LibraryPanel{ libraryPanel := LibraryPanel{
OrgID: c.SignedInUser.OrgId, OrgID: c.SignedInUser.OrgId,
@ -28,14 +28,10 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre
UpdatedBy: c.SignedInUser.UserId, UpdatedBy: c.SignedInUser.UserId,
} }
err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { 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 _, err := session.Insert(&libraryPanel); err != nil {
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
return errLibraryPanelAlreadyExists
}
return err return err
} }
return nil return nil
@ -44,7 +40,7 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre
return libraryPanel, err return libraryPanel, err
} }
// deleteLibraryPanel deletes a Library Panel // deleteLibraryPanel deletes a Library Panel.
func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid string) error { func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid string) error {
orgID := c.SignedInUser.OrgId orgID := c.SignedInUser.OrgId
return lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { 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. // getLibraryPanel gets a Library Panel.
func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string) (LibraryPanel, error) { func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string) (LibraryPanel, error) {
orgID := c.SignedInUser.OrgId
var libraryPanel LibraryPanel var libraryPanel LibraryPanel
err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error { err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
libraryPanels := make([]LibraryPanel, 0) var err error
err := session.SQL("SELECT * FROM library_panel WHERE uid=? and org_id=?", uid, orgID).Find(&libraryPanels) libraryPanel, err = getLibraryPanel(session, uid, c.SignedInUser.OrgId)
if err != nil { return err
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
}) })
return libraryPanel, 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) { func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext) ([]LibraryPanel, error) {
orgID := c.SignedInUser.OrgId orgID := c.SignedInUser.OrgId
libraryPanels := make([]LibraryPanel, 0) libraryPanels := make([]LibraryPanel, 0)
err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error { err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
err := session.SQL("SELECT * FROM library_panel WHERE org_id=?", orgID).Find(&libraryPanels) err := session.SQL("SELECT * FROM library_panel WHERE org_id=?", orgID).Find(&libraryPanels)
if err != nil { if err != nil {
@ -104,3 +104,50 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext) ([]Lib
return libraryPanels, err 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
}

View File

@ -60,7 +60,11 @@ func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) {
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, {Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
{Name: "updated_by", Type: migrator.DB_BigInt, 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("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]))
} }

View File

@ -5,9 +5,9 @@ import (
"testing" "testing"
"time" "time"
"gopkg.in/macaron.v1" "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry" "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 { type libraryPanel struct {
ID int64 `json:"id"` ID int64 `json:"id"`
OrgID int64 `json:"orgId"` OrgID int64 `json:"orgId"`
FolderID int64 `json:"folderId"` FolderID int64 `json:"folderId"`
UID string `json:"uid"` UID string `json:"uid"`
Name string `json:"name"` 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 { type libraryPanelResult struct {
@ -280,3 +497,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
fn(t, sc) fn(t, sc)
}) })
} }
func getCompareOptions() []cmp.Option {
return []cmp.Option{
cmp.Transformer("Time", func(in time.Time) int64 {
return in.UTC().Unix()
}),
}
}

View File

@ -23,8 +23,8 @@ type LibraryPanel struct {
} }
var ( var (
// errLibraryPanelAlreadyAdded is an error for when the user tries to add a library panel that already exists. // errLibraryPanelAlreadyExists 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 = errors.New("library panel with that name already exists")
// errLibraryPanelNotFound is an error for when a library panel can't be found. // errLibraryPanelNotFound is an error for when a library panel can't be found.
errLibraryPanelNotFound = errors.New("library panel could not be found") errLibraryPanelNotFound = errors.New("library panel could not be found")
) )
@ -37,3 +37,10 @@ type createLibraryPanelCommand struct {
Name string `json:"name"` Name string `json:"name"`
Model json.RawMessage `json:"model"` 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"`
}