From d84cfdbb0ff80df58b8b9305f0cc5e891beaadc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 2 Mar 2021 13:33:26 +0100 Subject: [PATCH] LibraryPanels: Adds version column (#31590) * Refactor: adds version column and fixes tests * Chore: adds version check when patching the library panel * Refactor: adds support for version in FrontEnd --- pkg/services/librarypanels/api.go | 98 ++++++------------- pkg/services/librarypanels/database.go | 12 ++- pkg/services/librarypanels/librarypanels.go | 6 +- .../librarypanels_create_test.go | 2 + .../librarypanels_get_all_test.go | 2 + .../librarypanels/librarypanels_get_test.go | 1 + .../librarypanels/librarypanels_patch_test.go | 29 +++++- .../librarypanels_permissions_test.go | 10 +- .../librarypanels/librarypanels_test.go | 6 +- pkg/services/librarypanels/models.go | 6 ++ .../SaveLibraryPanelModal.tsx | 5 +- .../app/features/library-panels/state/api.ts | 20 +++- 12 files changed, 113 insertions(+), 84 deletions(-) diff --git a/pkg/services/librarypanels/api.go b/pkg/services/librarypanels/api.go index 5f251c13bcd..e39e0589b24 100644 --- a/pkg/services/librarypanels/api.go +++ b/pkg/services/librarypanels/api.go @@ -33,16 +33,7 @@ func (lps *LibraryPanelService) registerAPIEndpoints() { func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd createLibraryPanelCommand) response.Response { panel, err := lps.createLibraryPanel(c, cmd) if err != nil { - if errors.Is(err, errLibraryPanelAlreadyExists) { - return response.Error(400, errLibraryPanelAlreadyExists.Error(), err) - } - if errors.Is(err, models.ErrFolderNotFound) { - return response.Error(404, models.ErrFolderNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderAccessDenied) { - return response.Error(403, models.ErrFolderAccessDenied.Error(), err) - } - return response.Error(500, "Failed to create library panel", err) + return toLibraryPanelError(err, "Failed to create library panel") } return response.JSON(200, util.DynMap{"result": panel}) @@ -50,17 +41,9 @@ func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd createLi // connectHandler handles POST /api/library-panels/:uid/dashboards/:dashboardId. func (lps *LibraryPanelService) connectHandler(c *models.ReqContext) response.Response { - if err := lps.connectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId")); err != nil { - if errors.Is(err, errLibraryPanelNotFound) { - return response.Error(404, errLibraryPanelNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderNotFound) { - return response.Error(404, models.ErrFolderNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderAccessDenied) { - return response.Error(403, models.ErrFolderAccessDenied.Error(), err) - } - return response.Error(500, "Failed to connect library panel", err) + err := lps.connectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId")) + if err != nil { + return toLibraryPanelError(err, "Failed to connect library panel") } return response.Success("Library panel connected") @@ -70,16 +53,7 @@ func (lps *LibraryPanelService) connectHandler(c *models.ReqContext) response.Re func (lps *LibraryPanelService) deleteHandler(c *models.ReqContext) response.Response { err := lps.deleteLibraryPanel(c, c.Params(":uid")) if err != nil { - if errors.Is(err, errLibraryPanelNotFound) { - return response.Error(404, errLibraryPanelNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderNotFound) { - return response.Error(404, models.ErrFolderNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderAccessDenied) { - return response.Error(403, models.ErrFolderAccessDenied.Error(), err) - } - return response.Error(500, "Failed to delete library panel", err) + return toLibraryPanelError(err, "Failed to delete library panel") } return response.Success("Library panel deleted") @@ -89,19 +63,7 @@ func (lps *LibraryPanelService) deleteHandler(c *models.ReqContext) response.Res func (lps *LibraryPanelService) disconnectHandler(c *models.ReqContext) response.Response { err := lps.disconnectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId")) if err != nil { - if errors.Is(err, errLibraryPanelNotFound) { - return response.Error(404, errLibraryPanelNotFound.Error(), err) - } - if errors.Is(err, errLibraryPanelDashboardNotFound) { - return response.Error(404, errLibraryPanelDashboardNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderNotFound) { - return response.Error(404, models.ErrFolderNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderAccessDenied) { - return response.Error(403, models.ErrFolderAccessDenied.Error(), err) - } - return response.Error(500, "Failed to disconnect library panel", err) + return toLibraryPanelError(err, "Failed to disconnect library panel") } return response.Success("Library panel disconnected") @@ -111,10 +73,7 @@ func (lps *LibraryPanelService) disconnectHandler(c *models.ReqContext) response func (lps *LibraryPanelService) getHandler(c *models.ReqContext) response.Response { libraryPanel, err := lps.getLibraryPanel(c, c.Params(":uid")) if err != nil { - if errors.Is(err, errLibraryPanelNotFound) { - return response.Error(404, errLibraryPanelNotFound.Error(), err) - } - return response.Error(500, "Failed to get library panel", err) + return toLibraryPanelError(err, "Failed to get library panel") } return response.JSON(200, util.DynMap{"result": libraryPanel}) @@ -124,7 +83,7 @@ func (lps *LibraryPanelService) getHandler(c *models.ReqContext) response.Respon func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Response { libraryPanels, err := lps.getAllLibraryPanels(c, c.QueryInt64("limit")) if err != nil { - return response.Error(500, "Failed to get library panels", err) + return toLibraryPanelError(err, "Failed to get library panels") } return response.JSON(200, util.DynMap{"result": libraryPanels}) @@ -134,10 +93,7 @@ func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Res func (lps *LibraryPanelService) getConnectedDashboardsHandler(c *models.ReqContext) response.Response { dashboardIDs, err := lps.getConnectedDashboards(c, c.Params(":uid")) if err != nil { - if errors.Is(err, errLibraryPanelNotFound) { - return response.Error(404, errLibraryPanelNotFound.Error(), err) - } - return response.Error(500, "Failed to get connected dashboards", err) + return toLibraryPanelError(err, "Failed to get connected dashboards") } return response.JSON(200, util.DynMap{"result": dashboardIDs}) @@ -147,20 +103,30 @@ func (lps *LibraryPanelService) getConnectedDashboardsHandler(c *models.ReqConte func (lps *LibraryPanelService) patchHandler(c *models.ReqContext, cmd patchLibraryPanelCommand) response.Response { libraryPanel, err := lps.patchLibraryPanel(c, cmd, c.Params(":uid")) if err != nil { - if errors.Is(err, errLibraryPanelAlreadyExists) { - return response.Error(400, errLibraryPanelAlreadyExists.Error(), err) - } - if errors.Is(err, errLibraryPanelNotFound) { - return response.Error(404, errLibraryPanelNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderNotFound) { - return response.Error(404, models.ErrFolderNotFound.Error(), err) - } - if errors.Is(err, models.ErrFolderAccessDenied) { - return response.Error(403, models.ErrFolderAccessDenied.Error(), err) - } - return response.Error(500, "Failed to update library panel", err) + return toLibraryPanelError(err, "Failed to update library panel") } return response.JSON(200, util.DynMap{"result": libraryPanel}) } + +func toLibraryPanelError(err error, message string) response.Response { + if errors.Is(err, errLibraryPanelAlreadyExists) { + return response.Error(400, errLibraryPanelAlreadyExists.Error(), err) + } + if errors.Is(err, errLibraryPanelNotFound) { + return response.Error(404, errLibraryPanelNotFound.Error(), err) + } + if errors.Is(err, errLibraryPanelDashboardNotFound) { + return response.Error(404, errLibraryPanelDashboardNotFound.Error(), err) + } + if errors.Is(err, errLibraryPanelVersionMismatch) { + return response.Error(412, errLibraryPanelVersionMismatch.Error(), err) + } + if errors.Is(err, models.ErrFolderNotFound) { + return response.Error(404, models.ErrFolderNotFound.Error(), err) + } + if errors.Is(err, models.ErrFolderAccessDenied) { + return response.Error(403, models.ErrFolderAccessDenied.Error(), err) + } + return response.Error(500, message, err) +} diff --git a/pkg/services/librarypanels/database.go b/pkg/services/librarypanels/database.go index a461ae605dc..783c0d4b6c9 100644 --- a/pkg/services/librarypanels/database.go +++ b/pkg/services/librarypanels/database.go @@ -15,7 +15,7 @@ import ( var ( sqlStatmentLibrayPanelDTOWithMeta = ` SELECT DISTINCT - lp.id, lp.org_id, lp.folder_id, lp.uid, lp.name, lp.model, lp.created, lp.created_by, lp.updated, lp.updated_by + lp.id, lp.org_id, lp.folder_id, lp.uid, lp.name, lp.model, lp.created, lp.created_by, lp.updated, lp.updated_by, lp.version , 0 AS can_edit , u1.login AS created_by_name , u1.email AS created_by_email @@ -53,6 +53,7 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre UID: util.GenerateShortUID(), Name: cmd.Name, Model: cmd.Model, + Version: 1, Created: time.Now(), Updated: time.Now(), @@ -85,6 +86,7 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre UID: libraryPanel.UID, Name: libraryPanel.Name, Model: libraryPanel.Model, + Version: libraryPanel.Version, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: 0, @@ -340,6 +342,7 @@ func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string UID: libraryPanel.UID, Name: libraryPanel.Name, Model: libraryPanel.Model, + Version: libraryPanel.Version, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: libraryPanel.ConnectedDashboards, @@ -395,6 +398,7 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, limit UID: panel.UID, Name: panel.Name, Model: panel.Model, + Version: panel.Version, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: panel.ConnectedDashboards, @@ -466,6 +470,7 @@ func (lps *LibraryPanelService) getLibraryPanelsForDashboardID(c *models.ReqCont UID: panel.UID, Name: panel.Name, Model: panel.Model, + Version: panel.Version, Meta: LibraryPanelDTOMeta{ CanEdit: panel.CanEdit, ConnectedDashboards: panel.ConnectedDashboards, @@ -522,6 +527,9 @@ func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patc if err != nil { return err } + if panelInDB.Version != cmd.Version { + return errLibraryPanelVersionMismatch + } var libraryPanel = LibraryPanel{ ID: panelInDB.ID, @@ -530,6 +538,7 @@ func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patc UID: uid, Name: cmd.Name, Model: cmd.Model, + Version: panelInDB.Version + 1, Created: panelInDB.Created, CreatedBy: panelInDB.CreatedBy, Updated: time.Now(), @@ -564,6 +573,7 @@ func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patc UID: libraryPanel.UID, Name: libraryPanel.Name, Model: libraryPanel.Model, + Version: libraryPanel.Version, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: panelInDB.ConnectedDashboards, diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 105bda9dc5f..db05fa42a9a 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -102,8 +102,9 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap()) elem.Set("id", panelAsJSON.Get("id").MustInt64()) elem.Set("libraryPanel", map[string]interface{}{ - "uid": libraryPanelInDB.UID, - "name": libraryPanelInDB.Name, + "uid": libraryPanelInDB.UID, + "name": libraryPanelInDB.Name, + "version": libraryPanelInDB.Version, "meta": map[string]interface{}{ "canEdit": libraryPanelInDB.Meta.CanEdit, "connectedDashboards": libraryPanelInDB.Meta.ConnectedDashboards, @@ -246,6 +247,7 @@ func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) { {Name: "created_by", Type: migrator.DB_BigInt, Nullable: false}, {Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, {Name: "updated_by", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "version", Type: migrator.DB_BigInt, Nullable: false}, }, Indices: []*migrator.Index{ {Cols: []string{"org_id", "folder_id", "name"}, Type: migrator.UniqueIndex}, diff --git a/pkg/services/librarypanels/librarypanels_create_test.go b/pkg/services/librarypanels/librarypanels_create_test.go index 50acc2de8cc..852e77a8c0d 100644 --- a/pkg/services/librarypanels/librarypanels_create_test.go +++ b/pkg/services/librarypanels/librarypanels_create_test.go @@ -30,6 +30,7 @@ func TestCreateLibraryPanel(t *testing.T) { "title": "Text - Library Panel", "type": "text", }, + Version: 1, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: 0, @@ -71,6 +72,7 @@ func TestCreateLibraryPanel(t *testing.T) { "title": "Library Panel Name", "type": "text", }, + Version: 1, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: 0, diff --git a/pkg/services/librarypanels/librarypanels_get_all_test.go b/pkg/services/librarypanels/librarypanels_get_all_test.go index 5e5f6e6b4aa..f7f3117176b 100644 --- a/pkg/services/librarypanels/librarypanels_get_all_test.go +++ b/pkg/services/librarypanels/librarypanels_get_all_test.go @@ -49,6 +49,7 @@ func TestGetAllLibraryPanels(t *testing.T) { "title": "Text - Library Panel", "type": "text", }, + Version: 1, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: 0, @@ -78,6 +79,7 @@ func TestGetAllLibraryPanels(t *testing.T) { "title": "Text - Library Panel2", "type": "text", }, + Version: 1, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: 0, diff --git a/pkg/services/librarypanels/librarypanels_get_test.go b/pkg/services/librarypanels/librarypanels_get_test.go index 20b586916b2..f6ba8057724 100644 --- a/pkg/services/librarypanels/librarypanels_get_test.go +++ b/pkg/services/librarypanels/librarypanels_get_test.go @@ -35,6 +35,7 @@ func TestGetLibraryPanel(t *testing.T) { "title": "Text - Library Panel", "type": "text", }, + Version: 1, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: 0, diff --git a/pkg/services/librarypanels/librarypanels_patch_test.go b/pkg/services/librarypanels/librarypanels_patch_test.go index c35e6b52cec..cd0627708ef 100644 --- a/pkg/services/librarypanels/librarypanels_patch_test.go +++ b/pkg/services/librarypanels/librarypanels_patch_test.go @@ -37,6 +37,7 @@ func TestPatchLibraryPanel(t *testing.T) { "type": "text" } `), + Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) @@ -55,6 +56,7 @@ func TestPatchLibraryPanel(t *testing.T) { "title": "Panel - New name", "type": "text", }, + Version: 2, Meta: LibraryPanelDTOMeta{ CanEdit: true, ConnectedDashboards: 2, @@ -83,6 +85,7 @@ func TestPatchLibraryPanel(t *testing.T) { newFolder := createFolderWithACL(t, "NewFolder", sc.user, []folderACLItem{}) cmd := patchLibraryPanelCommand{ FolderID: newFolder.Id, + Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.patchHandler(sc.reqContext, cmd) @@ -91,6 +94,7 @@ func TestPatchLibraryPanel(t *testing.T) { sc.initialResult.Result.FolderID = newFolder.Id sc.initialResult.Result.Meta.CreatedBy.Name = "user_in_db" sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = "/avatar/402d08de060496d6b6874495fe20f5ad" + sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } @@ -101,6 +105,7 @@ func TestPatchLibraryPanel(t *testing.T) { cmd := patchLibraryPanelCommand{ FolderID: -1, Name: "New Name", + Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.patchHandler(sc.reqContext, cmd) @@ -109,6 +114,7 @@ func TestPatchLibraryPanel(t *testing.T) { sc.initialResult.Result.Meta.CreatedBy.Name = "user_in_db" sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = "/avatar/402d08de060496d6b6874495fe20f5ad" sc.initialResult.Result.Model["title"] = "New Name" + sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } @@ -119,6 +125,7 @@ func TestPatchLibraryPanel(t *testing.T) { cmd := patchLibraryPanelCommand{ FolderID: -1, Model: []byte(`{ "title": "New Model Title", "name": "New Model Name" }`), + Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.patchHandler(sc.reqContext, cmd) @@ -129,6 +136,7 @@ func TestPatchLibraryPanel(t *testing.T) { } sc.initialResult.Result.Meta.CreatedBy.Name = "user_in_db" sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = "/avatar/402d08de060496d6b6874495fe20f5ad" + sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } @@ -136,7 +144,7 @@ func TestPatchLibraryPanel(t *testing.T) { scenarioWithLibraryPanel(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) { - cmd := patchLibraryPanelCommand{FolderID: -1} + cmd := patchLibraryPanelCommand{FolderID: -1, Version: 1} sc.reqContext.UserId = 2 sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.patchHandler(sc.reqContext, cmd) @@ -144,6 +152,7 @@ func TestPatchLibraryPanel(t *testing.T) { sc.initialResult.Result.Meta.UpdatedBy.ID = int64(2) sc.initialResult.Result.Meta.CreatedBy.Name = "user_in_db" sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = "/avatar/402d08de060496d6b6874495fe20f5ad" + sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } @@ -155,7 +164,8 @@ func TestPatchLibraryPanel(t *testing.T) { resp := sc.service.createHandler(sc.reqContext, command) var result = validateAndUnMarshalResponse(t, resp) cmd := patchLibraryPanelCommand{ - Name: "Text - Library Panel", + Name: "Text - Library Panel", + Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) @@ -170,6 +180,7 @@ func TestPatchLibraryPanel(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) cmd := patchLibraryPanelCommand{ FolderID: 1, + Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) @@ -180,10 +191,24 @@ func TestPatchLibraryPanel(t *testing.T) { func(t *testing.T, sc scenarioContext) { cmd := patchLibraryPanelCommand{ FolderID: sc.folder.Id, + Version: 1, } sc.reqContext.OrgId = 2 sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, 404, resp.Status()) }) + + scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with an old version number, it should fail", + func(t *testing.T, sc scenarioContext) { + cmd := patchLibraryPanelCommand{ + FolderID: sc.folder.Id, + Version: 1, + } + sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) + resp := sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 200, resp.Status()) + resp = sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 412, resp.Status()) + }) } diff --git a/pkg/services/librarypanels/librarypanels_permissions_test.go b/pkg/services/librarypanels/librarypanels_permissions_test.go index 3eaa8184bad..ee3b3008fdf 100644 --- a/pkg/services/librarypanels/librarypanels_permissions_test.go +++ b/pkg/services/librarypanels/librarypanels_permissions_test.go @@ -85,7 +85,7 @@ func TestLibraryPanelPermissions(t *testing.T) { toFolder := createFolderWithACL(t, "Folder", sc.user, testCase.items) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: toFolder.Id} + cmd := patchLibraryPanelCommand{FolderID: toFolder.Id, Version: 1} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, testCase.status, resp.Status()) @@ -100,7 +100,7 @@ func TestLibraryPanelPermissions(t *testing.T) { toFolder := createFolderWithACL(t, "Folder", sc.user, everyonePermissions) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: toFolder.Id} + cmd := patchLibraryPanelCommand{FolderID: toFolder.Id, Version: 1} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, testCase.status, resp.Status()) @@ -197,7 +197,7 @@ func TestLibraryPanelPermissions(t *testing.T) { result := validateAndUnMarshalResponse(t, resp) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: 0} + cmd := patchLibraryPanelCommand{FolderID: 0, Version: 1} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, testCase.status, resp.Status()) @@ -211,7 +211,7 @@ func TestLibraryPanelPermissions(t *testing.T) { result := validateAndUnMarshalResponse(t, resp) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: folder.Id} + cmd := patchLibraryPanelCommand{FolderID: folder.Id, Version: 1} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, testCase.status, resp.Status()) @@ -307,7 +307,7 @@ func TestLibraryPanelPermissions(t *testing.T) { result := validateAndUnMarshalResponse(t, resp) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: -100} + cmd := patchLibraryPanelCommand{FolderID: -100, Version: 1} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, 404, resp.Status()) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 5b806c5215b..501775ed83f 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -86,8 +86,9 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) { }, "datasource": "${DS_GDEV-TESTDATA}", "libraryPanel": map[string]interface{}{ - "uid": sc.initialResult.Result.UID, - "name": sc.initialResult.Result.Name, + "uid": sc.initialResult.Result.UID, + "name": sc.initialResult.Result.Name, + "version": sc.initialResult.Result.Version, "meta": map[string]interface{}{ "canEdit": false, "connectedDashboards": int64(1), @@ -669,6 +670,7 @@ type libraryPanel struct { UID string `json:"uid"` Name string `json:"name"` Model map[string]interface{} `json:"model"` + Version int64 `json:"version"` Meta LibraryPanelDTOMeta `json:"meta"` } diff --git a/pkg/services/librarypanels/models.go b/pkg/services/librarypanels/models.go index 681a37f23af..aba12b3acf4 100644 --- a/pkg/services/librarypanels/models.go +++ b/pkg/services/librarypanels/models.go @@ -14,6 +14,7 @@ type LibraryPanel struct { UID string `xorm:"uid"` Name string Model json.RawMessage + Version int64 Created time.Time Updated time.Time @@ -30,6 +31,7 @@ type LibraryPanelWithMeta struct { UID string `xorm:"uid"` Name string Model json.RawMessage + Version int64 Created time.Time Updated time.Time @@ -52,6 +54,7 @@ type LibraryPanelDTO struct { UID string `json:"uid"` Name string `json:"name"` Model json.RawMessage `json:"model"` + Version int64 `json:"version"` Meta LibraryPanelDTOMeta `json:"meta"` } @@ -98,6 +101,8 @@ var ( errLibraryPanelHeaderNameMissing = errors.New("library panel header is missing required property name") // ErrFolderHasConnectedLibraryPanels is an error for when an user deletes a folder that contains connected library panels. ErrFolderHasConnectedLibraryPanels = errors.New("folder contains library panels that are linked to dashboards") + // errLibraryPanelVersionMismatch is an error for when a library panel has been changed by someone else. + errLibraryPanelVersionMismatch = errors.New("the library panel has been changed by someone else") ) // Commands @@ -114,4 +119,5 @@ type patchLibraryPanelCommand struct { FolderID int64 `json:"folderId" binding:"Default(-1)"` Name string `json:"name"` Model json.RawMessage `json:"model"` + Version int64 `json:"version" binding:"Required"` } diff --git a/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx b/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx index d01ea79d8ec..6a3da834a4c 100644 --- a/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx @@ -5,11 +5,10 @@ import { css } from 'emotion'; import { useAsync, useDebounce } from 'react-use'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { usePanelSave } from '../../utils/usePanelSave'; -import { PanelModel } from 'app/features/dashboard/state'; -import { getLibraryPanelConnectedDashboards, LibraryPanelDTO } from '../../state/api'; +import { getLibraryPanelConnectedDashboards, PanelModelWithLibraryPanel } from '../../state/api'; interface Props { - panel: PanelModel & { libraryPanel: Pick }; + panel: PanelModelWithLibraryPanel; folderId: number; isOpen: boolean; onConfirm: () => void; diff --git a/public/app/features/library-panels/state/api.ts b/public/app/features/library-panels/state/api.ts index 1059d7d3248..96f46d888e9 100644 --- a/public/app/features/library-panels/state/api.ts +++ b/public/app/features/library-panels/state/api.ts @@ -1,4 +1,6 @@ -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; + +import { PanelModel } from '../../dashboard/state'; export interface LibraryPanelDTO { id: number; @@ -7,6 +9,7 @@ export interface LibraryPanelDTO { uid: string; name: string; model: any; + version: number; meta: LibraryPanelDTOMeta; } @@ -25,12 +28,19 @@ export interface LibraryPanelDTOMetaUser { avatarUrl: string; } +export interface PanelModelWithLibraryPanel extends PanelModel { + libraryPanel: Pick; +} + export async function getLibraryPanels(): Promise { const { result } = await getBackendSrv().get(`/api/library-panels`); return result; } -export async function addLibraryPanel(panelSaveModel: any, folderId: number): Promise { +export async function addLibraryPanel( + panelSaveModel: PanelModelWithLibraryPanel, + folderId: number +): Promise { const { result } = await getBackendSrv().post(`/api/library-panels`, { folderId, name: panelSaveModel.title, @@ -39,11 +49,15 @@ export async function addLibraryPanel(panelSaveModel: any, folderId: number): Pr return result; } -export async function updateLibraryPanel(panelSaveModel: any, folderId: number): Promise { +export async function updateLibraryPanel( + panelSaveModel: PanelModelWithLibraryPanel, + folderId: number +): Promise { const { result } = await getBackendSrv().patch(`/api/library-panels/${panelSaveModel.libraryPanel.uid}`, { folderId, name: panelSaveModel.title, model: panelSaveModel, + version: panelSaveModel.libraryPanel.version, }); return result; }