From d6449c16c7e84006bce33c2bd3988ca18b77ce79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 12 Jan 2021 12:57:40 +0100 Subject: [PATCH] LibraryPanels: adds connections (#30212) * LibraryPanels: adds connections * Chore: testing signing verification * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen * Chore: changes after PR comments * Chore: changes after PR comments Co-authored-by: Arve Knudsen --- pkg/services/librarypanels/api.go | 50 ++++++- pkg/services/librarypanels/database.go | 78 +++++++++++ pkg/services/librarypanels/librarypanels.go | 17 +++ .../librarypanels/librarypanels_test.go | 130 ++++++++++++++++++ pkg/services/librarypanels/models.go | 13 ++ 5 files changed, 285 insertions(+), 3 deletions(-) diff --git a/pkg/services/librarypanels/api.go b/pkg/services/librarypanels/api.go index 589a4a5fe25..600fd357afb 100644 --- a/pkg/services/librarypanels/api.go +++ b/pkg/services/librarypanels/api.go @@ -18,9 +18,12 @@ func (lps *LibraryPanelService) registerAPIEndpoints() { lps.RouteRegister.Group("/api/library-panels", func(libraryPanels routing.RouteRegister) { libraryPanels.Post("/", middleware.ReqSignedIn, binding.Bind(createLibraryPanelCommand{}), api.Wrap(lps.createHandler)) + libraryPanels.Post("/:uid/dashboards/:dashboardId", middleware.ReqSignedIn, api.Wrap(lps.connectHandler)) libraryPanels.Delete("/:uid", middleware.ReqSignedIn, api.Wrap(lps.deleteHandler)) + libraryPanels.Delete("/:uid/dashboards/:dashboardId", middleware.ReqSignedIn, api.Wrap(lps.disconnectHandler)) libraryPanels.Get("/", middleware.ReqSignedIn, api.Wrap(lps.getAllHandler)) libraryPanels.Get("/:uid", middleware.ReqSignedIn, api.Wrap(lps.getHandler)) + libraryPanels.Get("/:uid/dashboards/", middleware.ReqSignedIn, api.Wrap(lps.getConnectedDashboardsHandler)) libraryPanels.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryPanelCommand{}), api.Wrap(lps.patchHandler)) }) } @@ -38,7 +41,19 @@ func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd createLi return api.JSON(200, util.DynMap{"result": panel}) } -// deleteHandler handles DELETE /api/library-panels/:uid +// connectHandler handles POST /api/library-panels/:uid/dashboards/:dashboardId. +func (lps *LibraryPanelService) connectHandler(c *models.ReqContext) api.Response { + if err := lps.connectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId")); err != nil { + if errors.Is(err, errLibraryPanelNotFound) { + return api.Error(404, errLibraryPanelNotFound.Error(), err) + } + return api.Error(500, "Failed to connect library panel", err) + } + + return api.Success("Library panel connected") +} + +// deleteHandler handles DELETE /api/library-panels/:uid. func (lps *LibraryPanelService) deleteHandler(c *models.ReqContext) api.Response { err := lps.deleteLibraryPanel(c, c.Params(":uid")) if err != nil { @@ -51,7 +66,23 @@ func (lps *LibraryPanelService) deleteHandler(c *models.ReqContext) api.Response return api.Success("Library panel deleted") } -// getHandler handles GET /api/library-panels/:uid +// disconnectHandler handles DELETE /api/library-panels/:uid/dashboards/:dashboardId. +func (lps *LibraryPanelService) disconnectHandler(c *models.ReqContext) api.Response { + err := lps.disconnectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId")) + if err != nil { + if errors.Is(err, errLibraryPanelNotFound) { + return api.Error(404, errLibraryPanelNotFound.Error(), err) + } + if errors.Is(err, errLibraryPanelDashboardNotFound) { + return api.Error(404, errLibraryPanelDashboardNotFound.Error(), err) + } + return api.Error(500, "Failed to disconnect library panel", err) + } + + return api.Success("Library panel disconnected") +} + +// getHandler handles GET /api/library-panels/:uid. func (lps *LibraryPanelService) getHandler(c *models.ReqContext) api.Response { libraryPanel, err := lps.getLibraryPanel(c, c.Params(":uid")) if err != nil { @@ -64,7 +95,7 @@ func (lps *LibraryPanelService) getHandler(c *models.ReqContext) api.Response { return api.JSON(200, util.DynMap{"result": libraryPanel}) } -// getAllHandler handles GET /api/library-panels/ +// getAllHandler handles GET /api/library-panels/. func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) api.Response { libraryPanels, err := lps.getAllLibraryPanels(c) if err != nil { @@ -74,6 +105,19 @@ func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) api.Response return api.JSON(200, util.DynMap{"result": libraryPanels}) } +// getConnectedDashboardsHandler handles GET /api/library-panels/:uid/dashboards/. +func (lps *LibraryPanelService) getConnectedDashboardsHandler(c *models.ReqContext) api.Response { + dashboardIDs, err := lps.getConnectedDashboards(c, c.Params(":uid")) + if err != nil { + if errors.Is(err, errLibraryPanelNotFound) { + return api.Error(404, errLibraryPanelNotFound.Error(), err) + } + return api.Error(500, "Failed to get connected dashboards", err) + } + + return api.JSON(200, util.DynMap{"result": dashboardIDs}) +} + // 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")) diff --git a/pkg/services/librarypanels/database.go b/pkg/services/librarypanels/database.go index 27a2213ab81..d0c0f5a51a8 100644 --- a/pkg/services/librarypanels/database.go +++ b/pkg/services/librarypanels/database.go @@ -40,6 +40,34 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre return libraryPanel, err } +// connectDashboard adds a connection between a Library Panel and a Dashboard. +func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid string, dashboardID int64) error { + err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { + panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) + if err != nil { + return err + } + + // TODO add check that dashboard exists + + libraryPanelDashboard := libraryPanelDashboard{ + DashboardID: dashboardID, + LibraryPanelID: panel.ID, + Created: time.Now(), + CreatedBy: c.SignedInUser.UserId, + } + if _, err := session.Insert(&libraryPanelDashboard); err != nil { + if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return nil + } + return err + } + return nil + }) + + return err +} + // deleteLibraryPanel deletes a Library Panel. func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid string) error { orgID := c.SignedInUser.OrgId @@ -59,6 +87,29 @@ func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid str }) } +// disconnectDashboard deletes a connection between a Library Panel and a Dashboard. +func (lps *LibraryPanelService) disconnectDashboard(c *models.ReqContext, uid string, dashboardID int64) error { + return lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { + panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) + if err != nil { + return err + } + + result, err := session.Exec("DELETE FROM library_panel_dashboard WHERE librarypanel_id=? and dashboard_id=?", panel.ID, dashboardID) + if err != nil { + return err + } + + if rowsAffected, err := result.RowsAffected(); err != nil { + return err + } else if rowsAffected != 1 { + return errLibraryPanelDashboardNotFound + } + + return nil + }) +} + func getLibraryPanel(session *sqlstore.DBSession, uid string, orgID int64) (LibraryPanel, error) { libraryPanels := make([]LibraryPanel, 0) session.Table("library_panel") @@ -105,6 +156,33 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext) ([]Lib return libraryPanels, err } +// getConnectedDashboards gets all dashboards connected to a Library Panel. +func (lps *LibraryPanelService) getConnectedDashboards(c *models.ReqContext, uid string) ([]int64, error) { + connectedDashboardIDs := make([]int64, 0) + err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error { + panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) + if err != nil { + return err + } + + var libraryPanelDashboards []libraryPanelDashboard + session.Table("library_panel_dashboard") + session.Where("librarypanel_id=?", panel.ID) + err = session.Find(&libraryPanelDashboards) + if err != nil { + return err + } + + for _, lpd := range libraryPanelDashboards { + connectedDashboardIDs = append(connectedDashboardIDs, lpd.DashboardID) + } + + return nil + }) + + return connectedDashboardIDs, err +} + // patchLibraryPanel updates a Library Panel. func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patchLibraryPanelCommand, uid string) (LibraryPanel, error) { var libraryPanel LibraryPanel diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index cd708a6ad49..693d6a1410d 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -67,4 +67,21 @@ func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) { 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])) + + libraryPanelDashboardV1 := migrator.Table{ + Name: "library_panel_dashboard", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "librarypanel_id", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "dashboard_id", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "created_by", Type: migrator.DB_BigInt, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"librarypanel_id", "dashboard_id"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create library_panel_dashboard table v1", migrator.NewAddTableMigration(libraryPanelDashboardV1)) + mg.AddMigration("add index library_panel_dashboard librarypanel_id & dashboard_id", migrator.NewAddIndexMigration(libraryPanelDashboardV1, libraryPanelDashboardV1.Indices[0])) } diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 8b6d0372782..de46be833dd 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -27,6 +27,33 @@ func TestCreateLibraryPanel(t *testing.T) { }) } +func TestConnectLibraryPanel(t *testing.T) { + testScenario(t, "When an admin tries to create a connection for a library panel that does not exist, it should fail", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", "dashboardId": "1"}) + response := sc.service.connectHandler(sc.reqContext) + require.Equal(t, 404, response.Status()) + }) + + testScenario(t, "When an admin tries to create a connection that already 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 result libraryPanelResult + err := json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"}) + response = sc.service.connectHandler(sc.reqContext) + require.Equal(t, 200, response.Status()) + + response = sc.service.connectHandler(sc.reqContext) + require.Equal(t, 200, response.Status()) + }) +} + func TestDeleteLibraryPanel(t *testing.T) { testScenario(t, "When an admin tries to delete a library panel that does not exist, it should fail", func(t *testing.T, sc scenarioContext) { @@ -67,6 +94,47 @@ func TestDeleteLibraryPanel(t *testing.T) { }) } +func TestDisconnectLibraryPanel(t *testing.T) { + testScenario(t, "When an admin tries to remove a connection with a library panel that does not exist, it should fail", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", "dashboardId": "1"}) + response := sc.service.disconnectHandler(sc.reqContext) + require.Equal(t, 404, response.Status()) + }) + + testScenario(t, "When an admin tries to remove a connection that does not exist, 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) + + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"}) + response = sc.service.disconnectHandler(sc.reqContext) + require.Equal(t, 404, response.Status()) + }) + + testScenario(t, "When an admin tries to remove a connection that does exist, 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 result libraryPanelResult + err := json.Unmarshal(response.Body(), &result) + require.NoError(t, err) + + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"}) + response = sc.service.connectHandler(sc.reqContext) + require.Equal(t, 200, response.Status()) + response = sc.service.disconnectHandler(sc.reqContext) + require.Equal(t, 200, response.Status()) + }) +} + func TestGetLibraryPanel(t *testing.T) { testScenario(t, "When an admin tries to get a library panel that does not exist, it should fail", func(t *testing.T, sc scenarioContext) { @@ -178,6 +246,64 @@ func TestGetAllLibraryPanels(t *testing.T) { }) } +func TestGetConnectedDashboards(t *testing.T) { + testScenario(t, "When an admin tries to get connected dashboards for a library panel that does not exist, it should fail", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"}) + response := sc.service.getConnectedDashboardsHandler(sc.reqContext) + require.Equal(t, 404, response.Status()) + }) + + testScenario(t, "When an admin tries to get connected dashboards for a library panel that exists, but has no connections, it should return none", + 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) + + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) + response = sc.service.getConnectedDashboardsHandler(sc.reqContext) + require.Equal(t, 200, response.Status()) + + var dashResult libraryPanelDashboardsResult + err = json.Unmarshal(response.Body(), &dashResult) + require.NoError(t, err) + require.Equal(t, 0, len(dashResult.Result)) + }) + + testScenario(t, "When an admin tries to get connected dashboards for a library panel that exists and has connections, it should return connected dashboard IDs", + 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) + + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "11"}) + response = sc.service.connectHandler(sc.reqContext) + require.Equal(t, 200, response.Status()) + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "12"}) + response = sc.service.connectHandler(sc.reqContext) + require.Equal(t, 200, response.Status()) + + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) + response = sc.service.getConnectedDashboardsHandler(sc.reqContext) + require.Equal(t, 200, response.Status()) + + var dashResult libraryPanelDashboardsResult + err = json.Unmarshal(response.Body(), &dashResult) + require.NoError(t, err) + require.Equal(t, 2, len(dashResult.Result)) + require.Equal(t, int64(11), dashResult.Result[0]) + require.Equal(t, int64(12), dashResult.Result[1]) + }) +} + 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) { @@ -411,6 +537,10 @@ type libraryPanelsResult struct { Result []libraryPanel `json:"result"` } +type libraryPanelDashboardsResult struct { + Result []int64 `json:"result"` +} + func overrideLibraryPanelServiceInRegistry(cfg *setting.Cfg) LibraryPanelService { lps := LibraryPanelService{ SQLStore: nil, diff --git a/pkg/services/librarypanels/models.go b/pkg/services/librarypanels/models.go index bc384cb8229..79289ff0311 100644 --- a/pkg/services/librarypanels/models.go +++ b/pkg/services/librarypanels/models.go @@ -22,11 +22,24 @@ type LibraryPanel struct { UpdatedBy int64 } +// libraryPanelDashboard is the model for library panel connections. +type libraryPanelDashboard struct { + ID int64 `xorm:"pk autoincr 'id'"` + LibraryPanelID int64 `xorm:"librarypanel_id"` + DashboardID int64 `xorm:"dashboard_id"` + + Created time.Time + + CreatedBy int64 +} + var ( // 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") + // errLibraryPanelDashboardNotFound is an error for when a library panel connection can't be found. + errLibraryPanelDashboardNotFound = errors.New("library panel connection could not be found") ) // Commands