From f1b2c750e55eb84ddbe748b9ed43c39ca2246a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 11 May 2021 07:10:19 +0200 Subject: [PATCH] LibraryElements: Adds library elements api and tables (#33741) * WIP: intial structure * Refactor: adds create library element endpoint * Feature: adds delete library element * wip * Refactor: adds get api * Refactor: adds get all api * Refactor: adds patch api * Refactor: changes to library_element_connection * Refactor: add get connections api * wip: in the middle of refactor * wip * Refactor: consolidating both api:s * Refactor: points front end to library elements api * Tests: Fixes broken test * Fix: fixes delete library elements in folder and adds tests * Refactor: changes order of tabs in manage folder * Refactor: fixes so link does not cover whole card * Update pkg/services/libraryelements/libraryelements.go Co-authored-by: Arve Knudsen * Update pkg/services/libraryelements/libraryelements_permissions_test.go Co-authored-by: Arve Knudsen * Update pkg/services/libraryelements/database.go Co-authored-by: Arve Knudsen * Chore: changes after PR comments * Update libraryelements.go * Chore: updates after PR comments Co-authored-by: Arve Knudsen --- pkg/api/dashboard.go | 6 +- pkg/api/folder.go | 8 +- pkg/api/http_server.go | 4 +- pkg/services/libraryelements/api.go | 123 +++ pkg/services/libraryelements/database.go | 688 +++++++++++++ .../guard.go | 18 +- .../libraryelements/libraryelements.go | 139 +++ .../libraryelements_create_test.go} | 56 +- .../libraryelements_delete_test.go | 76 ++ .../libraryelements_get_all_test.go} | 902 ++++++++++-------- .../libraryelements_get_test.go | 75 ++ .../libraryelements_patch_test.go} | 152 +-- .../libraryelements_permissions_test.go} | 328 ++----- .../libraryelements/libraryelements_test.go | 353 +++++++ pkg/services/libraryelements/models.go | 190 ++++ .../writers.go | 34 +- pkg/services/librarypanels/api.go | 144 --- pkg/services/librarypanels/database.go | 702 -------------- pkg/services/librarypanels/librarypanels.go | 144 +-- .../librarypanels_connections_test.go | 97 -- .../librarypanels_delete_test.go | 44 - .../librarypanels/librarypanels_get_test.go | 91 -- .../librarypanels/librarypanels_test.go | 461 ++++----- pkg/services/librarypanels/models.go | 142 --- .../AddPanelWidget/AddPanelWidget.tsx | 4 +- .../folders/FolderLibraryPanelsPage.tsx | 4 +- public/app/features/folders/state/navModel.ts | 20 +- .../library-panels/LibraryPanelsPage.tsx | 4 +- .../DeleteLibraryPanelModal.tsx | 4 +- .../DeleteLibraryPanelModal/actions.ts | 4 +- .../LibraryPanelCard/LibraryPanelCard.tsx | 22 +- .../LibraryPanelsSearch.test.tsx | 20 +- .../LibraryPanelsSearch.tsx | 4 +- .../LibraryPanelsView/LibraryPanelsView.tsx | 6 +- .../components/LibraryPanelsView/actions.ts | 4 +- .../LibraryPanelsView/reducer.test.ts | 10 +- .../components/LibraryPanelsView/reducer.ts | 4 +- .../OpenLibraryPanelModal.tsx | 11 +- .../PanelLibraryOptionsGroup.tsx | 6 +- .../app/features/library-panels/state/api.ts | 51 +- public/app/features/library-panels/types.ts | 38 +- public/app/features/library-panels/utils.ts | 10 +- 42 files changed, 2758 insertions(+), 2445 deletions(-) create mode 100644 pkg/services/libraryelements/api.go create mode 100644 pkg/services/libraryelements/database.go rename pkg/services/{librarypanels => libraryelements}/guard.go (63%) create mode 100644 pkg/services/libraryelements/libraryelements.go rename pkg/services/{librarypanels/librarypanels_create_test.go => libraryelements/libraryelements_create_test.go} (59%) create mode 100644 pkg/services/libraryelements/libraryelements_delete_test.go rename pkg/services/{librarypanels/librarypanels_get_all_test.go => libraryelements/libraryelements_get_all_test.go} (51%) create mode 100644 pkg/services/libraryelements/libraryelements_get_test.go rename pkg/services/{librarypanels/librarypanels_patch_test.go => libraryelements/libraryelements_patch_test.go} (60%) rename pkg/services/{librarypanels/librarypanels_permissions_test.go => libraryelements/libraryelements_permissions_test.go} (57%) create mode 100644 pkg/services/libraryelements/libraryelements_test.go create mode 100644 pkg/services/libraryelements/models.go rename pkg/services/{librarypanels => libraryelements}/writers.go (62%) delete mode 100644 pkg/services/librarypanels/api.go delete mode 100644 pkg/services/librarypanels/database.go delete mode 100644 pkg/services/librarypanels/librarypanels_connections_test.go delete mode 100644 pkg/services/librarypanels/librarypanels_delete_test.go delete mode 100644 pkg/services/librarypanels/librarypanels_get_test.go diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index a7cedb356b3..ca6d88d875a 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -250,10 +250,10 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { } if hs.Cfg.IsPanelLibraryEnabled() { - // disconnect all library panels for this dashboard - err := hs.LibraryPanelService.DisconnectLibraryPanelsForDashboard(c, dash) + // disconnect all library elements for this dashboard + err := hs.LibraryElementService.DisconnectElementsFromDashboard(c, dash.Id) if err != nil { - hs.log.Error("Failed to disconnect library panels", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err) + hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err) } } diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 761b17bd11a..367c9b6ee68 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/grafana/grafana/pkg/services/librarypanels" + "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" @@ -89,10 +89,10 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext, cmd models.UpdateFolder func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) if hs.Cfg.IsPanelLibraryEnabled() { - err := hs.LibraryPanelService.DeleteLibraryPanelsInFolder(c, c.Params(":uid")) + err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c, c.Params(":uid")) if err != nil { - if errors.Is(err, librarypanels.ErrFolderHasConnectedLibraryPanels) { - return response.Error(403, "Folder could not be deleted because it contains linked library panels", err) + if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) { + return response.Error(403, "Folder could not be deleted because it contains library elements in use", err) } return ToFolderErrorResponse(err) } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index e8598e38568..f6d269de850 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -34,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/hooks" + "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live/pushhttp" @@ -48,8 +49,8 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" - "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/util/errutil" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" macaron "gopkg.in/macaron.v1" @@ -97,6 +98,7 @@ type HTTPServer struct { ContextHandler *contexthandler.ContextHandler `inject:""` SQLStore *sqlstore.SQLStore `inject:""` LibraryPanelService *librarypanels.LibraryPanelService `inject:""` + LibraryElementService *libraryelements.LibraryElementService `inject:""` DataService *tsdb.Service `inject:""` PluginDashboardService *plugindashboards.Service `inject:""` AlertEngine *alerting.AlertEngine `inject:""` diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go new file mode 100644 index 00000000000..55854883b2c --- /dev/null +++ b/pkg/services/libraryelements/api.go @@ -0,0 +1,123 @@ +package libraryelements + +import ( + "errors" + + "github.com/go-macaron/binding" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" +) + +func (l *LibraryElementService) registerAPIEndpoints() { + if !l.IsEnabled() { + return + } + + l.RouteRegister.Group("/api/library-elements", func(entities routing.RouteRegister) { + entities.Post("/", middleware.ReqSignedIn, binding.Bind(CreateLibraryElementCommand{}), routing.Wrap(l.createHandler)) + entities.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(l.deleteHandler)) + entities.Get("/", middleware.ReqSignedIn, routing.Wrap(l.getAllHandler)) + entities.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(l.getHandler)) + entities.Get("/:uid/connections/", middleware.ReqSignedIn, routing.Wrap(l.getConnectionsHandler)) + entities.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryElementCommand{}), routing.Wrap(l.patchHandler)) + }) +} + +// createHandler handles POST /api/library-elements. +func (l *LibraryElementService) createHandler(c *models.ReqContext, cmd CreateLibraryElementCommand) response.Response { + element, err := l.createLibraryElement(c, cmd) + if err != nil { + return toLibraryElementError(err, "Failed to create library element") + } + + return response.JSON(200, util.DynMap{"result": element}) +} + +// deleteHandler handles DELETE /api/library-elements/:uid. +func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Response { + err := l.deleteLibraryElement(c, c.Params(":uid")) + if err != nil { + return toLibraryElementError(err, "Failed to delete library element") + } + + return response.Success("Library element deleted") +} + +// getHandler handles GET /api/library-elements/:uid. +func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Response { + element, err := l.getLibraryElement(c, c.Params(":uid")) + if err != nil { + return toLibraryElementError(err, "Failed to get library element") + } + + return response.JSON(200, util.DynMap{"result": element}) +} + +// getAllHandler handles GET /api/library-elements/. +func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Response { + query := searchLibraryElementsQuery{ + perPage: c.QueryInt("perPage"), + page: c.QueryInt("page"), + searchString: c.Query("searchString"), + sortDirection: c.Query("sortDirection"), + kind: c.QueryInt("kind"), + typeFilter: c.Query("typeFilter"), + excludeUID: c.Query("excludeUid"), + folderFilter: c.Query("folderFilter"), + } + elementsResult, err := l.getAllLibraryElements(c, query) + if err != nil { + return toLibraryElementError(err, "Failed to get library elements") + } + + return response.JSON(200, util.DynMap{"result": elementsResult}) +} + +// patchHandler handles PATCH /api/library-elements/:uid +func (l *LibraryElementService) patchHandler(c *models.ReqContext, cmd patchLibraryElementCommand) response.Response { + element, err := l.patchLibraryElement(c, cmd, c.Params(":uid")) + if err != nil { + return toLibraryElementError(err, "Failed to update library element") + } + + return response.JSON(200, util.DynMap{"result": element}) +} + +// getConnectionsHandler handles GET /api/library-panels/:uid/connections/. +func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) response.Response { + connections, err := l.getConnections(c, c.Params(":uid")) + if err != nil { + return toLibraryElementError(err, "Failed to get connections") + } + + return response.JSON(200, util.DynMap{"result": connections}) +} + +func toLibraryElementError(err error, message string) response.Response { + if errors.Is(err, errLibraryElementAlreadyExists) { + return response.Error(400, errLibraryElementAlreadyExists.Error(), err) + } + if errors.Is(err, errLibraryElementNotFound) { + return response.Error(404, errLibraryElementNotFound.Error(), err) + } + if errors.Is(err, errLibraryElementDashboardNotFound) { + return response.Error(404, errLibraryElementDashboardNotFound.Error(), err) + } + if errors.Is(err, errLibraryElementVersionMismatch) { + return response.Error(412, errLibraryElementVersionMismatch.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) + } + if errors.Is(err, errLibraryElementHasConnections) { + return response.Error(403, errLibraryElementHasConnections.Error(), err) + } + return response.Error(500, message, err) +} diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go new file mode 100644 index 00000000000..ca6df368e3b --- /dev/null +++ b/pkg/services/libraryelements/database.go @@ -0,0 +1,688 @@ +package libraryelements + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/grafana/grafana/pkg/services/search" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/util" +) + +const ( + selectLibraryElementDTOWithMeta = ` +SELECT DISTINCT + le.name, le.id, le.org_id, le.folder_id, le.uid, le.kind, le.type, le.description, le.model, le.created, le.created_by, le.updated, le.updated_by, le.version + , u1.login AS created_by_name + , u1.email AS created_by_email + , u2.login AS updated_by_name + , u2.email AS updated_by_email + , (SELECT COUNT(connection_id) FROM ` + connectionTableName + ` WHERE library_element_id = le.id AND connection_kind=1) AS connections` + fromLibraryElementDTOWithMeta = ` +FROM library_element AS le + LEFT JOIN user AS u1 ON le.created_by = u1.id + LEFT JOIN user AS u2 ON le.updated_by = u2.id +` +) + +func syncFieldsWithModel(libraryElement *LibraryElement) error { + var model map[string]interface{} + if err := json.Unmarshal(libraryElement.Model, &model); err != nil { + return err + } + + if LibraryElementKind(libraryElement.Kind) == Panel { + model["title"] = libraryElement.Name + } else if LibraryElementKind(libraryElement.Kind) == Variable { + model["name"] = libraryElement.Name + } + if model["type"] != nil { + libraryElement.Type = model["type"].(string) + } else { + model["type"] = libraryElement.Type + } + if model["description"] != nil { + libraryElement.Description = model["description"].(string) + } else { + model["description"] = libraryElement.Description + } + syncedModel, err := json.Marshal(&model) + if err != nil { + return err + } + + libraryElement.Model = syncedModel + + return nil +} + +func getLibraryElement(session *sqlstore.DBSession, uid string, orgID int64) (LibraryElementWithMeta, error) { + elements := make([]LibraryElementWithMeta, 0) + sql := selectLibraryElementDTOWithMeta + + ", coalesce(dashboard.title, 'General') AS folder_name" + + ", coalesce(dashboard.uid, '') AS folder_uid" + + fromLibraryElementDTOWithMeta + + " LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" + + " WHERE le.uid=? AND le.org_id=?" + sess := session.SQL(sql, uid, orgID) + err := sess.Find(&elements) + if err != nil { + return LibraryElementWithMeta{}, err + } + if len(elements) == 0 { + return LibraryElementWithMeta{}, errLibraryElementNotFound + } + if len(elements) > 1 { + return LibraryElementWithMeta{}, fmt.Errorf("found %d elements, while expecting at most one", len(elements)) + } + + return elements[0], nil +} + +// createLibraryElement adds a library element. +func (l *LibraryElementService) createLibraryElement(c *models.ReqContext, cmd CreateLibraryElementCommand) (LibraryElementDTO, error) { + if err := l.requireSupportedElementKind(cmd.Kind); err != nil { + return LibraryElementDTO{}, err + } + element := LibraryElement{ + OrgID: c.SignedInUser.OrgId, + FolderID: cmd.FolderID, + UID: util.GenerateShortUID(), + Name: cmd.Name, + Model: cmd.Model, + Version: 1, + Kind: cmd.Kind, + + Created: time.Now(), + Updated: time.Now(), + + CreatedBy: c.SignedInUser.UserId, + UpdatedBy: c.SignedInUser.UserId, + } + + if err := syncFieldsWithModel(&element); err != nil { + return LibraryElementDTO{}, err + } + + err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + if err := l.requirePermissionsOnFolder(c.SignedInUser, cmd.FolderID); err != nil { + return err + } + if _, err := session.Insert(&element); err != nil { + if l.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return errLibraryElementAlreadyExists + } + return err + } + return nil + }) + + dto := LibraryElementDTO{ + ID: element.ID, + OrgID: element.OrgID, + FolderID: element.FolderID, + UID: element.UID, + Name: element.Name, + Kind: element.Kind, + Type: element.Type, + Description: element.Description, + Model: element.Model, + Version: element.Version, + Meta: LibraryElementDTOMeta{ + Connections: 0, + Created: element.Created, + Updated: element.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: element.CreatedBy, + Name: c.SignedInUser.Login, + AvatarURL: dtos.GetGravatarUrl(c.SignedInUser.Email), + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: element.UpdatedBy, + Name: c.SignedInUser.Login, + AvatarURL: dtos.GetGravatarUrl(c.SignedInUser.Email), + }, + }, + } + + return dto, err +} + +// deleteLibraryElement deletes a library element. +func (l *LibraryElementService) deleteLibraryElement(c *models.ReqContext, uid string) error { + return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + element, err := getLibraryElement(session, uid, c.SignedInUser.OrgId) + if err != nil { + return err + } + if err := l.requirePermissionsOnFolder(c.SignedInUser, element.FolderID); err != nil { + return err + } + var connectionIDs []struct { + ConnectionID int64 `xorm:"connection_id"` + } + sql := "SELECT connection_id FROM library_element_connection WHERE library_element_id=?" + if err := session.SQL(sql, element.ID).Find(&connectionIDs); err != nil { + return err + } else if len(connectionIDs) > 0 { + return errLibraryElementHasConnections + } + + result, err := session.Exec("DELETE FROM library_element WHERE id=?", element.ID) + if err != nil { + return err + } + if rowsAffected, err := result.RowsAffected(); err != nil { + return err + } else if rowsAffected != 1 { + return errLibraryElementNotFound + } + + return nil + }) +} + +// getLibraryElement gets a Library Element. +func (l *LibraryElementService) getLibraryElement(c *models.ReqContext, uid string) (LibraryElementDTO, error) { + var libraryElement LibraryElementWithMeta + err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + libraryElements := make([]LibraryElementWithMeta, 0) + builder := sqlstore.SQLBuilder{} + builder.Write(selectLibraryElementDTOWithMeta) + builder.Write(", 'General' as folder_name ") + builder.Write(", '' as folder_uid ") + builder.Write(fromLibraryElementDTOWithMeta) + builder.Write(` WHERE le.uid=? AND le.org_id=? AND le.folder_id=0`, uid, c.SignedInUser.OrgId) + builder.Write(" UNION ") + builder.Write(selectLibraryElementDTOWithMeta) + builder.Write(", dashboard.title as folder_name ") + builder.Write(", dashboard.uid as folder_uid ") + builder.Write(fromLibraryElementDTOWithMeta) + builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0") + builder.Write(` WHERE le.uid=? AND le.org_id=?`, uid, c.SignedInUser.OrgId) + if c.SignedInUser.OrgRole != models.ROLE_ADMIN { + builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW) + } + builder.Write(` OR dashboard.id=0`) + if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElements); err != nil { + return err + } + if len(libraryElements) == 0 { + return errLibraryElementNotFound + } + if len(libraryElements) > 1 { + return fmt.Errorf("found %d elements, while expecting at most one", len(libraryElements)) + } + + libraryElement = libraryElements[0] + + return nil + }) + if err != nil { + return LibraryElementDTO{}, err + } + + dto := LibraryElementDTO{ + ID: libraryElement.ID, + OrgID: libraryElement.OrgID, + FolderID: libraryElement.FolderID, + UID: libraryElement.UID, + Name: libraryElement.Name, + Kind: libraryElement.Kind, + Type: libraryElement.Type, + Description: libraryElement.Description, + Model: libraryElement.Model, + Version: libraryElement.Version, + Meta: LibraryElementDTOMeta{ + FolderName: libraryElement.FolderName, + FolderUID: libraryElement.FolderUID, + Connections: libraryElement.Connections, + Created: libraryElement.Created, + Updated: libraryElement.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: libraryElement.CreatedBy, + Name: libraryElement.CreatedByName, + AvatarURL: dtos.GetGravatarUrl(libraryElement.CreatedByEmail), + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: libraryElement.UpdatedBy, + Name: libraryElement.UpdatedByName, + AvatarURL: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail), + }, + }, + } + + return dto, nil +} + +// getAllLibraryElements gets all Library Elements. +func (l *LibraryElementService) getAllLibraryElements(c *models.ReqContext, query searchLibraryElementsQuery) (LibraryElementSearchResult, error) { + elements := make([]LibraryElementWithMeta, 0) + result := LibraryElementSearchResult{} + if query.perPage <= 0 { + query.perPage = 100 + } + if query.page <= 0 { + query.page = 1 + } + var typeFilter []string + if len(strings.TrimSpace(query.typeFilter)) > 0 { + typeFilter = strings.Split(query.typeFilter, ",") + } + folderFilter := parseFolderFilter(query) + if folderFilter.parseError != nil { + return LibraryElementSearchResult{}, folderFilter.parseError + } + err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + builder := sqlstore.SQLBuilder{} + if folderFilter.includeGeneralFolder { + builder.Write(selectLibraryElementDTOWithMeta) + builder.Write(", 'General' as folder_name ") + builder.Write(", '' as folder_uid ") + builder.Write(fromLibraryElementDTOWithMeta) + builder.Write(` WHERE le.org_id=? AND le.folder_id=0`, c.SignedInUser.OrgId) + writeKindSQL(query, &builder) + writeSearchStringSQL(query, l.SQLStore, &builder) + writeExcludeSQL(query, &builder) + writeTypeFilterSQL(typeFilter, &builder) + builder.Write(" UNION ") + } + builder.Write(selectLibraryElementDTOWithMeta) + builder.Write(", dashboard.title as folder_name ") + builder.Write(", dashboard.uid as folder_uid ") + builder.Write(fromLibraryElementDTOWithMeta) + builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id<>0") + builder.Write(` WHERE le.org_id=?`, c.SignedInUser.OrgId) + writeKindSQL(query, &builder) + writeSearchStringSQL(query, l.SQLStore, &builder) + writeExcludeSQL(query, &builder) + writeTypeFilterSQL(typeFilter, &builder) + if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil { + return err + } + if c.SignedInUser.OrgRole != models.ROLE_ADMIN { + builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW) + } + if query.sortDirection == search.SortAlphaDesc.Name { + builder.Write(" ORDER BY 1 DESC") + } else { + builder.Write(" ORDER BY 1 ASC") + } + writePerPageSQL(query, l.SQLStore, &builder) + if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&elements); err != nil { + return err + } + + retDTOs := make([]LibraryElementDTO, 0) + for _, element := range elements { + retDTOs = append(retDTOs, LibraryElementDTO{ + ID: element.ID, + OrgID: element.OrgID, + FolderID: element.FolderID, + UID: element.UID, + Name: element.Name, + Kind: element.Kind, + Type: element.Type, + Description: element.Description, + Model: element.Model, + Version: element.Version, + Meta: LibraryElementDTOMeta{ + FolderName: element.FolderName, + FolderUID: element.FolderUID, + Connections: element.Connections, + Created: element.Created, + Updated: element.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: element.CreatedBy, + Name: element.CreatedByName, + AvatarURL: dtos.GetGravatarUrl(element.CreatedByEmail), + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: element.UpdatedBy, + Name: element.UpdatedByName, + AvatarURL: dtos.GetGravatarUrl(element.UpdatedByEmail), + }, + }, + }) + } + + var libraryElements []LibraryElement + countBuilder := sqlstore.SQLBuilder{} + countBuilder.Write("SELECT * FROM library_element AS le") + countBuilder.Write(` WHERE le.org_id=?`, c.SignedInUser.OrgId) + writeKindSQL(query, &countBuilder) + writeSearchStringSQL(query, l.SQLStore, &countBuilder) + writeExcludeSQL(query, &countBuilder) + writeTypeFilterSQL(typeFilter, &countBuilder) + if err := folderFilter.writeFolderFilterSQL(true, &countBuilder); err != nil { + return err + } + if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&libraryElements); err != nil { + return err + } + + result = LibraryElementSearchResult{ + TotalCount: int64(len(libraryElements)), + Elements: retDTOs, + Page: query.page, + PerPage: query.perPage, + } + + return nil + }) + + return result, err +} + +func (l *LibraryElementService) handleFolderIDPatches(elementToPatch *LibraryElement, fromFolderID int64, toFolderID int64, user *models.SignedInUser) error { + // FolderID was not provided in the PATCH request + if toFolderID == -1 { + toFolderID = fromFolderID + } + + // FolderID was provided in the PATCH request + if toFolderID != -1 && toFolderID != fromFolderID { + if err := l.requirePermissionsOnFolder(user, toFolderID); err != nil { + return err + } + } + + // Always check permissions for the folder where library element resides + if err := l.requirePermissionsOnFolder(user, fromFolderID); err != nil { + return err + } + + elementToPatch.FolderID = toFolderID + + return nil +} + +// patchLibraryElement updates a Library Element. +func (l *LibraryElementService) patchLibraryElement(c *models.ReqContext, cmd patchLibraryElementCommand, uid string) (LibraryElementDTO, error) { + var dto LibraryElementDTO + if err := l.requireSupportedElementKind(cmd.Kind); err != nil { + return LibraryElementDTO{}, err + } + err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + elementInDB, err := getLibraryElement(session, uid, c.SignedInUser.OrgId) + if err != nil { + return err + } + if elementInDB.Version != cmd.Version { + return errLibraryElementVersionMismatch + } + + var libraryElement = LibraryElement{ + ID: elementInDB.ID, + OrgID: c.SignedInUser.OrgId, + FolderID: cmd.FolderID, + UID: uid, + Name: cmd.Name, + Kind: elementInDB.Kind, + Type: elementInDB.Type, + Description: elementInDB.Description, + Model: cmd.Model, + Version: elementInDB.Version + 1, + Created: elementInDB.Created, + CreatedBy: elementInDB.CreatedBy, + Updated: time.Now(), + UpdatedBy: c.SignedInUser.UserId, + } + + if cmd.Name == "" { + libraryElement.Name = elementInDB.Name + } + if cmd.Model == nil { + libraryElement.Model = elementInDB.Model + } + if err := l.handleFolderIDPatches(&libraryElement, elementInDB.FolderID, cmd.FolderID, c.SignedInUser); err != nil { + return err + } + if err := syncFieldsWithModel(&libraryElement); err != nil { + return err + } + if rowsAffected, err := session.ID(elementInDB.ID).Update(&libraryElement); err != nil { + if l.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return errLibraryElementAlreadyExists + } + return err + } else if rowsAffected != 1 { + return errLibraryElementNotFound + } + + dto = LibraryElementDTO{ + ID: libraryElement.ID, + OrgID: libraryElement.OrgID, + FolderID: libraryElement.FolderID, + UID: libraryElement.UID, + Name: libraryElement.Name, + Kind: libraryElement.Kind, + Type: libraryElement.Type, + Description: libraryElement.Description, + Model: libraryElement.Model, + Version: libraryElement.Version, + Meta: LibraryElementDTOMeta{ + Connections: elementInDB.Connections, + Created: libraryElement.Created, + Updated: libraryElement.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: elementInDB.CreatedBy, + Name: elementInDB.CreatedByName, + AvatarURL: dtos.GetGravatarUrl(elementInDB.CreatedByEmail), + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: libraryElement.UpdatedBy, + Name: c.SignedInUser.Login, + AvatarURL: dtos.GetGravatarUrl(c.SignedInUser.Email), + }, + }, + } + + return nil + }) + + return dto, err +} + +// getConnections gets all connections for a Library Element. +func (l *LibraryElementService) getConnections(c *models.ReqContext, uid string) ([]LibraryElementConnectionDTO, error) { + connections := make([]LibraryElementConnectionDTO, 0) + err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + element, err := getLibraryElement(session, uid, c.SignedInUser.OrgId) + if err != nil { + return err + } + var libraryElementConnections []libraryElementConnectionWithMeta + builder := sqlstore.SQLBuilder{} + builder.Write("SELECT lec.*, u1.login AS created_by_name, u1.email AS created_by_email") + builder.Write(" FROM " + connectionTableName + " AS lec") + builder.Write(" LEFT JOIN user AS u1 ON lec.created_by = u1.id") + builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id") + builder.Write(` WHERE lec.library_element_id=?`, element.ID) + if c.SignedInUser.OrgRole != models.ROLE_ADMIN { + builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW) + } + if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElementConnections); err != nil { + return err + } + + for _, connection := range libraryElementConnections { + connections = append(connections, LibraryElementConnectionDTO{ + ID: connection.ID, + Kind: connection.ConnectionKind, + ElementID: connection.LibraryElementID, + ConnectionID: connection.ConnectionID, + Created: connection.Created, + CreatedBy: LibraryElementDTOMetaUser{ + ID: connection.CreatedBy, + Name: connection.CreatedByName, + AvatarURL: dtos.GetGravatarUrl(connection.CreatedByEmail), + }, + }) + } + + return nil + }) + + return connections, err +} + +//getElementsForDashboardID gets all elements for a specific dashboard +func (l *LibraryElementService) getElementsForDashboardID(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error) { + libraryElementMap := make(map[string]LibraryElementDTO) + err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + var libraryElements []LibraryElementWithMeta + sql := selectLibraryElementDTOWithMeta + + ", coalesce(dashboard.title, 'General') AS folder_name" + + ", coalesce(dashboard.uid, '') AS folder_uid" + + fromLibraryElementDTOWithMeta + + " LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" + + " INNER JOIN " + connectionTableName + " AS lce ON lce.library_element_id = le.id AND lce.connection_kind=1 AND lce.connection_id=?" + sess := session.SQL(sql, dashboardID) + err := sess.Find(&libraryElements) + if err != nil { + return err + } + + for _, element := range libraryElements { + libraryElementMap[element.UID] = LibraryElementDTO{ + ID: element.ID, + OrgID: element.OrgID, + FolderID: element.FolderID, + UID: element.UID, + Name: element.Name, + Kind: element.Kind, + Type: element.Type, + Description: element.Description, + Model: element.Model, + Version: element.Version, + Meta: LibraryElementDTOMeta{ + FolderName: element.FolderName, + FolderUID: element.FolderUID, + Connections: element.Connections, + Created: element.Created, + Updated: element.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: element.CreatedBy, + Name: element.CreatedByName, + AvatarURL: dtos.GetGravatarUrl(element.CreatedByEmail), + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: element.UpdatedBy, + Name: element.UpdatedByName, + AvatarURL: dtos.GetGravatarUrl(element.UpdatedByEmail), + }, + }, + } + } + + return nil + }) + + return libraryElementMap, err +} + +// connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard. +func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContext, elementUIDs []string, dashboardID int64) error { + err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + _, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE connection_kind=1 AND connection_id=?", dashboardID) + if err != nil { + return err + } + for _, elementUID := range elementUIDs { + element, err := getLibraryElement(session, elementUID, c.SignedInUser.OrgId) + if err != nil { + return err + } + if err := l.requirePermissionsOnFolder(c.SignedInUser, element.FolderID); err != nil { + return err + } + + connection := libraryElementConnection{ + LibraryElementID: element.ID, + ConnectionKind: 1, + ConnectionID: dashboardID, + Created: time.Now(), + CreatedBy: c.SignedInUser.UserId, + } + if _, err := session.Insert(&connection); err != nil { + if l.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return nil + } + return err + } + } + return nil + }) + + return err +} + +// disconnectElementsFromDashboardID deletes connections for all Library Elements in a Dashboard. +func (l *LibraryElementService) disconnectElementsFromDashboardID(c *models.ReqContext, dashboardID int64) error { + return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + _, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE connection_kind=1 AND connection_id=?", dashboardID) + if err != nil { + return err + } + return nil + }) +} + +// deleteLibraryElementsInFolderUID deletes all Library Elements in a folder. +func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c *models.ReqContext, folderUID string) error { + return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + var folderUIDs []struct { + ID int64 `xorm:"id"` + } + err := session.SQL("SELECT id from dashboard WHERE uid=? AND org_id=? AND is_folder=1", folderUID, c.SignedInUser.OrgId).Find(&folderUIDs) + if err != nil { + return err + } + if len(folderUIDs) != 1 { + return fmt.Errorf("found %d folders, while expecting at most one", len(folderUIDs)) + } + folderID := folderUIDs[0].ID + + if err := l.requirePermissionsOnFolder(c.SignedInUser, folderID); err != nil { + return err + } + var connectionIDs []struct { + ConnectionID int64 `xorm:"connection_id"` + } + sql := "SELECT lec.connection_id FROM library_element AS le" + sql += " INNER JOIN " + connectionTableName + " AS lec on le.id = lec.library_element_id" + sql += " WHERE le.folder_id=? AND le.org_id=?" + err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&connectionIDs) + if err != nil { + return err + } + if len(connectionIDs) > 0 { + return ErrFolderHasConnectedLibraryElements + } + + var elementIDs []struct { + ID int64 `xorm:"id"` + } + err = session.SQL("SELECT id from library_element WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId).Find(&elementIDs) + if err != nil { + return err + } + for _, elementID := range elementIDs { + _, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE library_element_id=?", elementID.ID) + if err != nil { + return err + } + } + if _, err := session.Exec("DELETE FROM library_element WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId); err != nil { + return err + } + + return nil + }) +} diff --git a/pkg/services/librarypanels/guard.go b/pkg/services/libraryelements/guard.go similarity index 63% rename from pkg/services/librarypanels/guard.go rename to pkg/services/libraryelements/guard.go index 0392a95ef9d..c5d41295c0c 100644 --- a/pkg/services/librarypanels/guard.go +++ b/pkg/services/libraryelements/guard.go @@ -1,4 +1,4 @@ -package librarypanels +package libraryelements import ( "github.com/grafana/grafana/pkg/models" @@ -10,7 +10,19 @@ func isGeneralFolder(folderID int64) bool { return folderID == 0 } -func (lps *LibraryPanelService) requirePermissionsOnFolder(user *models.SignedInUser, folderID int64) error { +func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) error { + kind := LibraryElementKind(kindAsInt) + switch kind { + case Panel: + return nil + case Variable: + return nil + default: + return errLibraryElementUnSupportedElementKind + } +} + +func (l *LibraryElementService) requirePermissionsOnFolder(user *models.SignedInUser, folderID int64) error { if isGeneralFolder(folderID) && user.HasRole(models.ROLE_EDITOR) { return nil } @@ -19,7 +31,7 @@ func (lps *LibraryPanelService) requirePermissionsOnFolder(user *models.SignedIn return models.ErrFolderAccessDenied } - s := dashboards.NewFolderService(user.OrgId, user, lps.SQLStore) + s := dashboards.NewFolderService(user.OrgId, user, l.SQLStore) folder, err := s.GetFolderByID(folderID) if err != nil { return err diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go new file mode 100644 index 00000000000..9c3a3d223c4 --- /dev/null +++ b/pkg/services/libraryelements/libraryelements.go @@ -0,0 +1,139 @@ +package libraryelements + +import ( + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/setting" +) + +// LibraryElementService is the service for the Library Element feature. +type LibraryElementService struct { + Cfg *setting.Cfg `inject:""` + SQLStore *sqlstore.SQLStore `inject:""` + RouteRegister routing.RouteRegister `inject:""` + log log.Logger +} + +const connectionTableName = "library_element_connection" + +func init() { + registry.RegisterService(&LibraryElementService{}) +} + +// Init initializes the LibraryElement service +func (l *LibraryElementService) Init() error { + l.log = log.New("library-elements") + + l.registerAPIEndpoints() + + return nil +} + +// IsEnabled returns true if the Panel Library feature is enabled for this instance. +func (l *LibraryElementService) IsEnabled() bool { + if l.Cfg == nil { + return false + } + + return l.Cfg.IsPanelLibraryEnabled() +} + +// CreateElement creates a Library Element. +func (l *LibraryElementService) CreateElement(c *models.ReqContext, cmd CreateLibraryElementCommand) (LibraryElementDTO, error) { + if !l.IsEnabled() { + return LibraryElementDTO{}, nil + } + + return l.createLibraryElement(c, cmd) +} + +// GetElementsForDashboard gets all connected elements for a specific dashboard. +func (l *LibraryElementService) GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error) { + if !l.IsEnabled() { + return map[string]LibraryElementDTO{}, nil + } + + return l.getElementsForDashboardID(c, dashboardID) +} + +// ConnectElementsToDashboard connects elements to a specific dashboard. +func (l *LibraryElementService) ConnectElementsToDashboard(c *models.ReqContext, elementUIDs []string, dashboardID int64) error { + if !l.IsEnabled() { + return nil + } + + return l.connectElementsToDashboardID(c, elementUIDs, dashboardID) +} + +// DisconnectElementsFromDashboard disconnects elements from a specific dashboard. +func (l *LibraryElementService) DisconnectElementsFromDashboard(c *models.ReqContext, dashboardID int64) error { + if !l.IsEnabled() { + return nil + } + + return l.disconnectElementsFromDashboardID(c, dashboardID) +} + +// DeleteLibraryElementsInFolder deletes all elements for a specific folder. +func (l *LibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error { + if !l.IsEnabled() { + return nil + } + return l.deleteLibraryElementsInFolderUID(c, folderUID) +} + +// AddMigration defines database migrations. +// If Panel Library is not enabled does nothing. +func (l *LibraryElementService) AddMigration(mg *migrator.Migrator) { + if !l.IsEnabled() { + return + } + + libraryElementsV1 := migrator.Table{ + Name: "library_element", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "folder_id", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, + {Name: "name", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, + {Name: "kind", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "type", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, + {Name: "description", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, + {Name: "model", Type: migrator.DB_Text, Nullable: false}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + {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", "kind"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create library_element table v1", migrator.NewAddTableMigration(libraryElementsV1)) + mg.AddMigration("add index library_element", migrator.NewAddIndexMigration(libraryElementsV1, libraryElementsV1.Indices[0])) + + libraryElementConnectionV1 := migrator.Table{ + Name: connectionTableName, + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "library_element_id", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "connection_kind", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "connection_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{"library_element_id", "connection_kind", "connection_id"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create "+connectionTableName+" table v1", migrator.NewAddTableMigration(libraryElementConnectionV1)) + mg.AddMigration("add index "+connectionTableName, migrator.NewAddIndexMigration(libraryElementConnectionV1, libraryElementConnectionV1.Indices[0])) +} diff --git a/pkg/services/librarypanels/librarypanels_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go similarity index 59% rename from pkg/services/librarypanels/librarypanels_create_test.go rename to pkg/services/libraryelements/libraryelements_create_test.go index 240c633f078..61fd4ab86af 100644 --- a/pkg/services/librarypanels/librarypanels_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -1,4 +1,4 @@ -package librarypanels +package libraryelements import ( "testing" @@ -7,23 +7,24 @@ import ( "github.com/stretchr/testify/require" ) -func TestCreateLibraryPanel(t *testing.T) { - scenarioWithLibraryPanel(t, "When an admin tries to create a library panel that already exists, it should fail", +func TestCreateLibraryElement(t *testing.T) { + scenarioWithPanel(t, "When an admin tries to create a library panel that already exists, it should fail", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel") + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 400, resp.Status()) }) - scenarioWithLibraryPanel(t, "When an admin tries to create a library panel that does not exists, it should succeed", + scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists, it should succeed", func(t *testing.T, sc scenarioContext) { - var expected = libraryPanelResult{ - Result: libraryPanel{ + var expected = libraryElementResult{ + Result: libraryElement{ ID: 1, OrgID: 1, FolderID: 1, UID: sc.initialResult.Result.UID, Name: "Text - Library Panel", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -34,20 +35,19 @@ func TestCreateLibraryPanel(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - ConnectedDashboards: 0, - Created: sc.initialResult.Result.Meta.Created, - Updated: sc.initialResult.Result.Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + Connections: 0, + Created: sc.initialResult.Result.Meta.Created, + Updated: sc.initialResult.Result.Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, Name: "signed_in_user", - AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, Name: "signed_in_user", - AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, @@ -59,16 +59,17 @@ func TestCreateLibraryPanel(t *testing.T) { testScenario(t, "When an admin tries to create a library panel where name and panel title differ, it should update panel title", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(1, "Library Panel Name") + command := getCreatePanelCommand(1, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) var result = validateAndUnMarshalResponse(t, resp) - var expected = libraryPanelResult{ - Result: libraryPanel{ + var expected = libraryElementResult{ + Result: libraryElement{ ID: 1, OrgID: 1, FolderID: 1, UID: result.Result.UID, Name: "Library Panel Name", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -79,20 +80,19 @@ func TestCreateLibraryPanel(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - ConnectedDashboards: 0, - Created: result.Result.Meta.Created, - Updated: result.Result.Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + Connections: 0, + Created: result.Result.Meta.Created, + Updated: result.Result.Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, Name: "signed_in_user", - AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, Name: "signed_in_user", - AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go new file mode 100644 index 00000000000..a3ca83cc3c9 --- /dev/null +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -0,0 +1,76 @@ +package libraryelements + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" +) + +func TestDeleteLibraryElement(t *testing.T) { + scenarioWithPanel(t, "When an admin tries to delete a library panel that does not exist, it should fail", + func(t *testing.T, sc scenarioContext) { + resp := sc.service.deleteHandler(sc.reqContext) + require.Equal(t, 404, resp.Status()) + }) + + scenarioWithPanel(t, "When an admin tries to delete a library panel that exists, it should succeed", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) + resp := sc.service.deleteHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + }) + + scenarioWithPanel(t, "When an admin tries to delete a library panel in another org, it should fail", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) + sc.reqContext.SignedInUser.OrgId = 2 + sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN + resp := sc.service.deleteHandler(sc.reqContext) + require.Equal(t, 404, resp.Status()) + }) + + scenarioWithPanel(t, "When an admin tries to delete a library panel that is connected, it should fail", + func(t *testing.T, sc scenarioContext) { + dashJSON := map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": int64(1), + "gridPos": map[string]interface{}{ + "h": 6, + "w": 6, + "x": 0, + "y": 0, + }, + }, + map[string]interface{}{ + "id": int64(2), + "gridPos": map[string]interface{}{ + "h": 6, + "w": 6, + "x": 6, + "y": 0, + }, + "libraryPanel": map[string]interface{}{ + "uid": sc.initialResult.Result.UID, + "name": sc.initialResult.Result.Name, + }, + }, + }, + } + dash := models.Dashboard{ + Title: "Testing deleteHandler ", + Data: simplejson.NewFromAny(dashJSON), + } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id) + require.NoError(t, err) + + sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) + resp := sc.service.deleteHandler(sc.reqContext) + require.Equal(t, 403, resp.Status()) + }) +} diff --git a/pkg/services/librarypanels/librarypanels_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go similarity index 51% rename from pkg/services/librarypanels/librarypanels_get_all_test.go rename to pkg/services/libraryelements/libraryelements_get_all_test.go index a2548c4e951..9302406e4b2 100644 --- a/pkg/services/librarypanels/librarypanels_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -1,33 +1,32 @@ -package librarypanels +package libraryelements import ( "encoding/json" "strconv" "testing" - "github.com/grafana/grafana/pkg/services/search" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/search" ) -func TestGetAllLibraryPanels(t *testing.T) { +func TestGetAllLibraryElements(t *testing.T) { testScenario(t, "When an admin tries to get all library panels and none exists, it should return none", func(t *testing.T, sc scenarioContext) { resp := sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err := json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ - TotalCount: 0, - LibraryPanels: []libraryPanel{}, - Page: 1, - PerPage: 100, + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 0, + Elements: []libraryElement{}, + Page: 1, + PerPage: 100, }, } if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { @@ -35,30 +34,35 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist, it should succeed", + scenarioWithPanel(t, "When an admin tries to get all panel elements and both panels and variables exist, it should only return panels", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreateVariableCommand(sc.folder.Id, "query0") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) + err := sc.reqContext.Req.ParseForm() + require.NoError(t, err) + sc.reqContext.Req.Form.Add("kind", strconv.FormatInt(int64(Panel), 10)) + resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch - err := json.Unmarshal(resp.Body(), &result) + var result libraryElementsSearch + err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ - TotalCount: 2, + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 1, Page: 1, PerPage: 100, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 1, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -69,57 +73,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, - }, - }, - }, - { - ID: 2, - OrgID: 1, - FolderID: 1, - UID: result.Result.LibraryPanels[1].UID, - Name: "Text - Library Panel2", - Type: "text", - Description: "A description", - Model: map[string]interface{}{ - "datasource": "${DS_GDEV-TESTDATA}", - "description": "A description", - "id": float64(1), - "title": "Text - Library Panel2", - "type": "text", - }, - Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[1].Meta.Created, - Updated: result.Result.LibraryPanels[1].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ - ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, - }, - UpdatedBy: LibraryPanelDTOMetaUser{ - ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -131,9 +99,169 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and sort desc is set, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all variable elements and both panels and variables exist, it should only return panels", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreateVariableCommand(sc.folder.Id, "query0") + resp := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, resp.Status()) + + err := sc.reqContext.Req.ParseForm() + require.NoError(t, err) + sc.reqContext.Req.Form.Add("kind", strconv.FormatInt(int64(Variable), 10)) + + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err = json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 1, + Page: 1, + PerPage: 100, + Elements: []libraryElement{ + { + ID: 2, + OrgID: 1, + FolderID: 1, + UID: result.Result.Elements[0].UID, + Name: "query0", + Kind: int64(Variable), + Type: "query", + Description: "A description", + Model: map[string]interface{}{ + "datasource": "${DS_GDEV-TESTDATA}", + "name": "query0", + "type": "query", + "description": "A description", + }, + Version: 1, + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: 1, + Name: userInDbName, + AvatarURL: userInDbAvatar, + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: 1, + Name: userInDbName, + AvatarURL: userInDbAvatar, + }, + }, + }, + }, + }, + } + if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist, it should succeed", + func(t *testing.T, sc scenarioContext) { + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") + resp := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, resp.Status()) + + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err := json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 2, + Page: 1, + PerPage: 100, + Elements: []libraryElement{ + { + ID: 1, + OrgID: 1, + FolderID: 1, + UID: result.Result.Elements[0].UID, + Name: "Text - Library Panel", + Kind: int64(Panel), + Type: "text", + Description: "A description", + Model: map[string]interface{}{ + "datasource": "${DS_GDEV-TESTDATA}", + "description": "A description", + "id": float64(1), + "title": "Text - Library Panel", + "type": "text", + }, + Version: 1, + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: 1, + Name: userInDbName, + AvatarURL: userInDbAvatar, + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: 1, + Name: userInDbName, + AvatarURL: userInDbAvatar, + }, + }, + }, + { + ID: 2, + OrgID: 1, + FolderID: 1, + UID: result.Result.Elements[1].UID, + Name: "Text - Library Panel2", + Kind: int64(Panel), + Type: "text", + Description: "A description", + Model: map[string]interface{}{ + "datasource": "${DS_GDEV-TESTDATA}", + "description": "A description", + "id": float64(1), + "title": "Text - Library Panel2", + "type": "text", + }, + Version: 1, + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[1].Meta.Created, + Updated: result.Result.Elements[1].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: 1, + Name: userInDbName, + AvatarURL: userInDbAvatar, + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: 1, + Name: userInDbName, + AvatarURL: userInDbAvatar, + }, + }, + }, + }, + }, + } + if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and sort desc is set, it should succeed and the result should be correct", + func(t *testing.T, sc scenarioContext) { + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) @@ -143,21 +271,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 2, Page: 1, PerPage: 100, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 2, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -168,22 +297,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -191,8 +319,9 @@ func TestGetAllLibraryPanels(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[1].UID, + UID: result.Result.Elements[1].UID, Name: "Text - Library Panel", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -203,22 +332,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[1].Meta.Created, - Updated: result.Result.LibraryPanels[1].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[1].Meta.Created, + Updated: result.Result.Elements[1].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -230,9 +358,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and panelFilter is set to existing types, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to existing types, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", []byte(` + command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", Panel, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -244,7 +372,7 @@ func TestGetAllLibraryPanels(t *testing.T) { resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) - command = getCreateCommandWithModel(sc.folder.Id, "BarGauge - Library Panel", []byte(` + command = getCreateCommandWithModel(sc.folder.Id, "BarGauge - Library Panel", Panel, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -258,25 +386,26 @@ func TestGetAllLibraryPanels(t *testing.T) { err := sc.reqContext.Req.ParseForm() require.NoError(t, err) - sc.reqContext.Req.Form.Add("panelFilter", "bargauge,gauge") + sc.reqContext.Req.Form.Add("typeFilter", "bargauge,gauge") resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 2, Page: 1, PerPage: 100, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 3, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "BarGauge - Library Panel", + Kind: int64(Panel), Type: "bargauge", Description: "BarGauge description", Model: map[string]interface{}{ @@ -287,22 +416,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "bargauge", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -310,8 +438,9 @@ func TestGetAllLibraryPanels(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[1].UID, + UID: result.Result.Elements[1].UID, Name: "Gauge - Library Panel", + Kind: int64(Panel), Type: "gauge", Description: "Gauge description", Model: map[string]interface{}{ @@ -322,22 +451,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "description": "Gauge description", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[1].Meta.Created, - Updated: result.Result.LibraryPanels[1].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[1].Meta.Created, + Updated: result.Result.Elements[1].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -349,9 +477,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and panelFilter is set to non existing type, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to a nonexistent type, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", []byte(` + command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", Panel, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -365,19 +493,19 @@ func TestGetAllLibraryPanels(t *testing.T) { err := sc.reqContext.Req.ParseForm() require.NoError(t, err) - sc.reqContext.Req.Form.Add("panelFilter", "unknown1,unknown2") + sc.reqContext.Req.Form.Add("typeFilter", "unknown1,unknown2") resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ - TotalCount: 0, - Page: 1, - PerPage: 100, - LibraryPanels: []libraryPanel{}, + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 0, + Page: 1, + PerPage: 100, + Elements: []libraryElement{}, }, } if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { @@ -385,10 +513,10 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to existing folders, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to existing folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) - command := getCreateCommand(newFolder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(newFolder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) folderFilter := strconv.FormatInt(newFolder.Id, 10) @@ -399,21 +527,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 1, Page: 1, PerPage: 100, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 2, OrgID: 1, FolderID: newFolder.Id, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -424,22 +553,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "NewFolder", - FolderUID: newFolder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "NewFolder", + FolderUID: newFolder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -451,10 +579,10 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to non existing folders, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to a nonexistent folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) - command := getCreateCommand(newFolder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(newFolder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) folderFilter := "2020,2021" @@ -465,15 +593,15 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ - TotalCount: 0, - Page: 1, - PerPage: 100, - LibraryPanels: []libraryPanel{}, + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 0, + Page: 1, + PerPage: 100, + Elements: []libraryElement{}, }, } if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { @@ -481,9 +609,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to General folder, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to General folder, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) folderFilter := "0" @@ -494,21 +622,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 0, Page: 1, PerPage: 100, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 1, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -519,22 +648,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -542,8 +670,9 @@ func TestGetAllLibraryPanels(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[1].UID, + UID: result.Result.Elements[1].UID, Name: "Text - Library Panel2", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -554,22 +683,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[1].Meta.Created, - Updated: result.Result.LibraryPanels[1].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[1].Meta.Created, + Updated: result.Result.Elements[1].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -581,9 +709,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and excludeUID is set, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and excludeUID is set, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) @@ -593,21 +721,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 1, Page: 1, PerPage: 100, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 2, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -618,22 +747,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -645,9 +773,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) @@ -657,21 +785,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 2, Page: 1, PerPage: 1, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 1, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -682,22 +811,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -709,9 +837,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 2, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 2, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) @@ -722,21 +850,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 2, Page: 2, PerPage: 1, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 2, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -747,22 +876,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -774,9 +902,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in the description, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in the description, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommandWithModel(sc.folder.Id, "Text - Library Panel2", []byte(` + command := getCreateCommandWithModel(sc.folder.Id, "Text - Library Panel2", Panel, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -796,21 +924,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 1, Page: 1, PerPage: 1, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 1, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -821,22 +950,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -848,9 +976,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in both name and description, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in both name and description, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommandWithModel(sc.folder.Id, "Some Other", []byte(` + command := getCreateCommandWithModel(sc.folder.Id, "Some Other", Panel, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -868,21 +996,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 2, Page: 1, PerPage: 100, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 2, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Some Other", + Kind: int64(Panel), Type: "text", Description: "A Library Panel", Model: map[string]interface{}{ @@ -893,22 +1022,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -916,8 +1044,9 @@ func TestGetAllLibraryPanels(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[1].UID, + UID: result.Result.Elements[1].UID, Name: "Text - Library Panel", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -928,22 +1057,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[1].Meta.Created, - Updated: result.Result.LibraryPanels[1].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[1].Meta.Created, + Updated: result.Result.Elements[1].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -955,9 +1083,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 1 and searchString is panel2, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 1 and searchString is panel2, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) @@ -969,21 +1097,22 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ TotalCount: 1, Page: 1, PerPage: 1, - LibraryPanels: []libraryPanel{ + Elements: []libraryElement{ { ID: 2, OrgID: 1, FolderID: 1, - UID: result.Result.LibraryPanels[0].UID, + UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", + Kind: int64(Panel), Type: "text", Description: "A description", Model: map[string]interface{}{ @@ -994,22 +1123,21 @@ func TestGetAllLibraryPanels(t *testing.T) { "type": "text", }, Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.LibraryPanels[0].Meta.Created, - Updated: result.Result.LibraryPanels[0].Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Elements[0].Meta.Created, + Updated: result.Result.Elements[0].Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, }, }, @@ -1021,9 +1149,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString is panel, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString is panel, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) @@ -1035,15 +1163,15 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ - TotalCount: 2, - Page: 3, - PerPage: 1, - LibraryPanels: []libraryPanel{}, + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 2, + Page: 3, + PerPage: 1, + Elements: []libraryElement{}, }, } if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { @@ -1051,9 +1179,9 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString does not exist, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString does not exist, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel2") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 200, resp.Status()) @@ -1065,15 +1193,15 @@ func TestGetAllLibraryPanels(t *testing.T) { resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ - TotalCount: 0, - Page: 3, - PerPage: 1, - LibraryPanels: []libraryPanel{}, + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 0, + Page: 3, + PerPage: 1, + Elements: []libraryElement{}, }, } if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { @@ -1081,56 +1209,32 @@ func TestGetAllLibraryPanels(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist but only one is connected, it should succeed and return correct connected dashboards", - func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") - resp := sc.service.createHandler(sc.reqContext, command) - require.Equal(t, 200, resp.Status()) - var result = validateAndUnMarshalResponse(t, resp) - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "1"}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "2"}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - resp = sc.service.getAllHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - var results libraryPanelsSearch - err := json.Unmarshal(resp.Body(), &results) - require.NoError(t, err) - require.Equal(t, int64(0), results.Result.LibraryPanels[0].Meta.ConnectedDashboards) - require.Equal(t, int64(2), results.Result.LibraryPanels[1].Meta.ConnectedDashboards) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to get all library panels in a different org, none should be returned", + scenarioWithPanel(t, "When an admin tries to get all library panels in a different org, none should be returned", func(t *testing.T, sc scenarioContext) { resp := sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch + var result libraryElementsSearch err := json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - require.Equal(t, 1, len(result.Result.LibraryPanels)) - require.Equal(t, int64(1), result.Result.LibraryPanels[0].FolderID) - require.Equal(t, "Text - Library Panel", result.Result.LibraryPanels[0].Name) + require.Equal(t, 1, len(result.Result.Elements)) + require.Equal(t, int64(1), result.Result.Elements[0].FolderID) + require.Equal(t, "Text - Library Panel", result.Result.Elements[0].Name) sc.reqContext.SignedInUser.OrgId = 2 sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - result = libraryPanelsSearch{} + result = libraryElementsSearch{} err = json.Unmarshal(resp.Body(), &result) require.NoError(t, err) - var expected = libraryPanelsSearch{ - Result: libraryPanelsSearchResult{ - TotalCount: 0, - LibraryPanels: []libraryPanel{}, - Page: 1, - PerPage: 100, + var expected = libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 0, + Elements: []libraryElement{}, + Page: 1, + PerPage: 100, }, } if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go new file mode 100644 index 00000000000..d85dddbe762 --- /dev/null +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -0,0 +1,75 @@ +package libraryelements + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" +) + +func TestGetLibraryElement(t *testing.T) { + scenarioWithPanel(t, "When an admin tries to get a library panel that does not exist, it should fail", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"}) + resp := sc.service.getHandler(sc.reqContext) + require.Equal(t, 404, resp.Status()) + }) + + scenarioWithPanel(t, "When an admin tries to get a library panel that exists, it should succeed and return correct result", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) + resp := sc.service.getHandler(sc.reqContext) + var result = validateAndUnMarshalResponse(t, resp) + var expected = libraryElementResult{ + Result: libraryElement{ + ID: 1, + OrgID: 1, + FolderID: 1, + UID: result.Result.UID, + Name: "Text - Library Panel", + Kind: int64(Panel), + Type: "text", + Description: "A description", + Model: map[string]interface{}{ + "datasource": "${DS_GDEV-TESTDATA}", + "description": "A description", + "id": float64(1), + "title": "Text - Library Panel", + "type": "text", + }, + Version: 1, + Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: sc.folder.Uid, + Connections: 0, + Created: result.Result.Meta.Created, + Updated: result.Result.Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ + ID: 1, + Name: userInDbName, + AvatarURL: userInDbAvatar, + }, + UpdatedBy: LibraryElementDTOMetaUser{ + ID: 1, + Name: userInDbName, + AvatarURL: userInDbAvatar, + }, + }, + }, + } + if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + scenarioWithPanel(t, "When an admin tries to get a library panel that exists in an other org, it should fail", + func(t *testing.T, sc scenarioContext) { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) + sc.reqContext.SignedInUser.OrgId = 2 + sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN + resp := sc.service.getHandler(sc.reqContext) + require.Equal(t, 404, resp.Status()) + }) +} diff --git a/pkg/services/librarypanels/librarypanels_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go similarity index 60% rename from pkg/services/librarypanels/librarypanels_patch_test.go rename to pkg/services/libraryelements/libraryelements_patch_test.go index 7206bb7a7d4..6864b3bc6e0 100644 --- a/pkg/services/librarypanels/librarypanels_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -1,4 +1,4 @@ -package librarypanels +package libraryelements import ( "testing" @@ -7,26 +7,19 @@ import ( "github.com/stretchr/testify/require" ) -func TestPatchLibraryPanel(t *testing.T) { - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel that does not exist, it should fail", +func TestPatchLibraryElement(t *testing.T) { + scenarioWithPanel(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{} + cmd := patchLibraryElementCommand{Kind: int64(Panel)} sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"}) resp := sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, 404, resp.Status()) }) - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel that exists, it should succeed", + scenarioWithPanel(t, "When an admin tries to patch a library panel that exists, it should succeed", func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "2"}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ FolderID: newFolder.Id, Name: "Panel - New name", Model: []byte(` @@ -38,19 +31,21 @@ func TestPatchLibraryPanel(t *testing.T) { "type": "graph" } `), + Kind: int64(Panel), Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp = sc.service.patchHandler(sc.reqContext, cmd) + resp := sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, 200, resp.Status()) var result = validateAndUnMarshalResponse(t, resp) - var expected = libraryPanelResult{ - Result: libraryPanel{ + var expected = libraryElementResult{ + Result: libraryElement{ ID: 1, OrgID: 1, FolderID: newFolder.Id, UID: sc.initialResult.Result.UID, Name: "Panel - New name", + Kind: int64(Panel), Type: "graph", Description: "An updated description", Model: map[string]interface{}{ @@ -61,20 +56,19 @@ func TestPatchLibraryPanel(t *testing.T) { "type": "graph", }, Version: 2, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - ConnectedDashboards: 2, - Created: sc.initialResult.Result.Meta.Created, - Updated: result.Result.Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ + Meta: LibraryElementDTOMeta{ + Connections: 0, + Created: sc.initialResult.Result.Meta.Created, + Updated: result.Result.Meta.Updated, + CreatedBy: LibraryElementDTOMetaUser{ ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, + Name: userInDbName, + AvatarURL: userInDbAvatar, }, - UpdatedBy: LibraryPanelDTOMetaUser{ + UpdatedBy: LibraryElementDTOMetaUser{ ID: 1, Name: "signed_in_user", - AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, @@ -84,11 +78,12 @@ func TestPatchLibraryPanel(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result", + scenarioWithPanel(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) { newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ FolderID: newFolder.Id, + Kind: int64(Panel), Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) @@ -96,27 +91,28 @@ func TestPatchLibraryPanel(t *testing.T) { require.Equal(t, 200, resp.Status()) var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.FolderID = newFolder.Id - sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName + sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar 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) } }) - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with name only, it should change name successfully, sync title and return correct result", + scenarioWithPanel(t, "When an admin tries to patch a library panel with name only, it should change name successfully, sync title and return correct result", func(t *testing.T, sc scenarioContext) { - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ FolderID: -1, Name: "New Name", + Kind: int64(Panel), Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.patchHandler(sc.reqContext, cmd) var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.Name = "New Name" - sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName + sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar sc.initialResult.Result.Model["title"] = "New Name" sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -124,11 +120,12 @@ func TestPatchLibraryPanel(t *testing.T) { } }) - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with model only, it should change model successfully, sync name, type and description fields and return correct result", + scenarioWithPanel(t, "When an admin tries to patch a library panel with model only, it should change model successfully, sync name, type and description fields and return correct result", func(t *testing.T, sc scenarioContext) { - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ FolderID: -1, Model: []byte(`{ "title": "New Model Title", "name": "New Model Name", "type":"graph", "description": "New description" }`), + Kind: int64(Panel), Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) @@ -142,19 +139,20 @@ func TestPatchLibraryPanel(t *testing.T) { "type": "graph", "description": "New description", } - sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName + sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar 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) } }) - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with model.description only, it should change model successfully, sync name, type and description fields and return correct result", + scenarioWithPanel(t, "When an admin tries to patch a library panel with model.description only, it should change model successfully, sync name, type and description fields and return correct result", func(t *testing.T, sc scenarioContext) { - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ FolderID: -1, Model: []byte(`{ "description": "New description" }`), + Kind: int64(Panel), Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) @@ -167,19 +165,20 @@ func TestPatchLibraryPanel(t *testing.T) { "type": "text", "description": "New description", } - sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName + sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar 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) } }) - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with model.type only, it should change model successfully, sync name, type and description fields and return correct result", + scenarioWithPanel(t, "When an admin tries to patch a library panel with model.type only, it should change model successfully, sync name, type and description fields and return correct result", func(t *testing.T, sc scenarioContext) { - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ FolderID: -1, Model: []byte(`{ "type": "graph" }`), + Kind: int64(Panel), Version: 1, } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) @@ -192,64 +191,67 @@ func TestPatchLibraryPanel(t *testing.T) { "type": "graph", "description": "A description", } - sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName + sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar 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) } }) - scenarioWithLibraryPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result", + scenarioWithPanel(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, Version: 1} + cmd := patchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(Panel)} sc.reqContext.UserId = 2 sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.patchHandler(sc.reqContext, cmd) var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.Meta.UpdatedBy.ID = int64(2) - sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName + sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar 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) } }) - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should fail", + scenarioWithPanel(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(sc.folder.Id, "Another Panel") + command := getCreatePanelCommand(sc.folder.Id, "Another Panel") resp := sc.service.createHandler(sc.reqContext, command) var result = validateAndUnMarshalResponse(t, resp) - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ Name: "Text - Library Panel", Version: 1, + Kind: int64(Panel), } sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, 400, resp.Status()) }) - scenarioWithLibraryPanel(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", + scenarioWithPanel(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) { newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) - command := getCreateCommand(newFolder.Id, "Text - Library Panel") + command := getCreatePanelCommand(newFolder.Id, "Text - Library Panel") resp := sc.service.createHandler(sc.reqContext, command) var result = validateAndUnMarshalResponse(t, resp) - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ FolderID: 1, Version: 1, + Kind: int64(Panel), } sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, 400, resp.Status()) }) - scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel in another org, it should fail", + scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail", func(t *testing.T, sc scenarioContext) { - cmd := patchLibraryPanelCommand{ + cmd := patchLibraryElementCommand{ FolderID: sc.folder.Id, Version: 1, + Kind: int64(Panel), } sc.reqContext.OrgId = 2 sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) @@ -257,11 +259,12 @@ func TestPatchLibraryPanel(t *testing.T) { 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", + scenarioWithPanel(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{ + cmd := patchLibraryElementCommand{ FolderID: sc.folder.Id, Version: 1, + Kind: int64(Panel), } sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.patchHandler(sc.reqContext, cmd) @@ -269,4 +272,33 @@ func TestPatchLibraryPanel(t *testing.T) { resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, 412, resp.Status()) }) + + scenarioWithPanel(t, "When an admin tries to patch a library panel with an other kind, it should succeed but panel should not change", + func(t *testing.T, sc scenarioContext) { + cmd := patchLibraryElementCommand{ + FolderID: sc.folder.Id, + Version: 1, + Kind: int64(Variable), + } + sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) + resp := sc.service.patchHandler(sc.reqContext, cmd) + require.Equal(t, 200, resp.Status()) + var result = validateAndUnMarshalResponse(t, resp) + sc.initialResult.Result.Type = "text" + sc.initialResult.Result.Kind = int64(Panel) + sc.initialResult.Result.Description = "A description" + sc.initialResult.Result.Model = map[string]interface{}{ + "datasource": "${DS_GDEV-TESTDATA}", + "id": float64(1), + "title": "Text - Library Panel", + "type": "text", + "description": "A description", + } + sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName + sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + 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) + } + }) } diff --git a/pkg/services/librarypanels/librarypanels_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go similarity index 57% rename from pkg/services/librarypanels/librarypanels_permissions_test.go rename to pkg/services/libraryelements/libraryelements_permissions_test.go index 7a4e2ab6580..3a2ca821d69 100644 --- a/pkg/services/librarypanels/librarypanels_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -1,18 +1,17 @@ -package librarypanels +package libraryelements import ( "encoding/json" "fmt" - "strconv" "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" - "github.com/stretchr/testify/require" ) -func TestLibraryPanelPermissions(t *testing.T) { +func TestLibraryElementPermissions(t *testing.T) { var defaultPermissions = []folderACLItem{} var adminOnlyPermissions = []folderACLItem{{models.ROLE_ADMIN, models.PERMISSION_EDIT}} var editorOnlyPermissions = []folderACLItem{{models.ROLE_EDITOR, models.PERMISSION_EDIT}} @@ -71,7 +70,7 @@ func TestLibraryPanelPermissions(t *testing.T) { folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) sc.reqContext.SignedInUser.OrgRole = testCase.role - command := getCreateCommand(folder.Id, "Library Panel Name") + command := getCreatePanelCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, testCase.status, resp.Status()) }) @@ -79,13 +78,13 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { fromFolder := createFolderWithACL(t, sc.sqlStore, "Everyone", sc.user, everyonePermissions) - command := getCreateCommand(fromFolder.Id, "Library Panel Name") + command := getCreatePanelCommand(fromFolder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: toFolder.Id, Version: 1} + cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(Panel)} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, testCase.status, resp.Status()) @@ -94,13 +93,13 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { fromFolder := createFolderWithACL(t, sc.sqlStore, "Everyone", sc.user, testCase.items) - command := getCreateCommand(fromFolder.Id, "Library Panel Name") + command := getCreatePanelCommand(fromFolder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: toFolder.Id, Version: 1} + cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(Panel)} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, testCase.status, resp.Status()) @@ -109,7 +108,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) - cmd := getCreateCommand(folder.Id, "Library Panel Name") + cmd := getCreatePanelCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) sc.reqContext.SignedInUser.OrgRole = testCase.role @@ -118,56 +117,6 @@ func TestLibraryPanelPermissions(t *testing.T) { resp = sc.service.deleteHandler(sc.reqContext) require.Equal(t, testCase.status, resp.Status()) }) - - testScenario(t, fmt.Sprintf("When %s tries to connect a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc), - func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) - dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id) - cmd := getCreateCommand(folder.Id, "Library Panel Name") - resp := sc.service.createHandler(sc.reqContext, cmd) - result := validateAndUnMarshalResponse(t, resp) - sc.reqContext.SignedInUser.OrgRole = testCase.role - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, testCase.status, resp.Status()) - }) - - testScenario(t, fmt.Sprintf("When %s tries to disconnect a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc), - func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) - dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id) - cmd := getCreateCommand(folder.Id, "Library Panel Name") - resp := sc.service.createHandler(sc.reqContext, cmd) - result := validateAndUnMarshalResponse(t, resp) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - sc.reqContext.SignedInUser.OrgRole = testCase.role - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) - resp = sc.service.disconnectHandler(sc.reqContext) - require.Equal(t, testCase.status, resp.Status()) - }) - - testScenario(t, fmt.Sprintf("When %s tries to delete all library panels in a folder with %s, it should return correct status", testCase.role, testCase.desc), - func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) - cmd := getCreateCommand(folder.Id, "Library Panel Name") - resp := sc.service.createHandler(sc.reqContext, cmd) - validateAndUnMarshalResponse(t, resp) - sc.reqContext.SignedInUser.OrgRole = testCase.role - - err := sc.service.DeleteLibraryPanelsInFolder(sc.reqContext, folder.Uid) - switch testCase.status { - case 200: - require.NoError(t, err) - case 403: - require.EqualError(t, err, models.ErrFolderAccessDenied.Error()) - default: - t.Fatalf("Unrecognized test case status %d", testCase.status) - } - }) } var generalFolderCases = []struct { @@ -184,7 +133,7 @@ func TestLibraryPanelPermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { sc.reqContext.SignedInUser.OrgRole = testCase.role - command := getCreateCommand(0, "Library Panel Name") + command := getCreatePanelCommand(0, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, testCase.status, resp.Status()) }) @@ -192,12 +141,12 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) - command := getCreateCommand(folder.Id, "Library Panel Name") + command := getCreatePanelCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: 0, Version: 1} + cmd := patchLibraryElementCommand{FolderID: 0, Version: 1, Kind: int64(Panel)} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, testCase.status, resp.Status()) @@ -206,12 +155,12 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) - command := getCreateCommand(0, "Library Panel Name") + command := getCreatePanelCommand(0, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: folder.Id, Version: 1} + cmd := patchLibraryElementCommand{FolderID: folder.Id, Version: 1, Kind: int64(Panel)} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, testCase.status, resp.Status()) @@ -219,7 +168,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - cmd := getCreateCommand(0, "Library Panel Name") + cmd := getCreatePanelCommand(0, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) sc.reqContext.SignedInUser.OrgRole = testCase.role @@ -228,57 +177,6 @@ func TestLibraryPanelPermissions(t *testing.T) { resp = sc.service.deleteHandler(sc.reqContext) require.Equal(t, testCase.status, resp.Status()) }) - - testScenario(t, fmt.Sprintf("When %s tries to connect a library panel in the General folder, it should return correct status", testCase.role), - func(t *testing.T, sc scenarioContext) { - dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0) - cmd := getCreateCommand(0, "Library Panel Name") - resp := sc.service.createHandler(sc.reqContext, cmd) - result := validateAndUnMarshalResponse(t, resp) - sc.reqContext.SignedInUser.OrgRole = testCase.role - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, testCase.status, resp.Status()) - }) - - testScenario(t, fmt.Sprintf("When %s tries to disconnect a library panel in the General folder, it should return correct status", testCase.role), - func(t *testing.T, sc scenarioContext) { - dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0) - cmd := getCreateCommand(0, "Library Panel Name") - resp := sc.service.createHandler(sc.reqContext, cmd) - result := validateAndUnMarshalResponse(t, resp) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - sc.reqContext.SignedInUser.OrgRole = testCase.role - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) - resp = sc.service.disconnectHandler(sc.reqContext) - require.Equal(t, testCase.status, resp.Status()) - }) - - testScenario(t, fmt.Sprintf("When %s tries to get connected dashboards in the General folder for a library panel in the General folder, it should return correct status", testCase.role), - func(t *testing.T, sc scenarioContext) { - dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0) - cmd := getCreateCommand(0, "Library Panel Name") - resp := sc.service.createHandler(sc.reqContext, cmd) - result := validateAndUnMarshalResponse(t, resp) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - sc.reqContext.SignedInUser.OrgRole = testCase.role - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) - resp = sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - var dashResult libraryPanelDashboardsResult - err := json.Unmarshal(resp.Body(), &dashResult) - require.NoError(t, err) - require.Equal(t, 200, resp.Status()) - require.Equal(t, 1, len(dashResult.Result)) - require.Equal(t, dashboard.Id, dashResult.Result[0]) - }) } var missingFolderCases = []struct { @@ -294,7 +192,7 @@ func TestLibraryPanelPermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { sc.reqContext.SignedInUser.OrgRole = testCase.role - command := getCreateCommand(-100, "Library Panel Name") + command := getCreatePanelCommand(-100, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) require.Equal(t, 404, resp.Status()) }) @@ -302,18 +200,78 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", testCase.role), func(t *testing.T, sc scenarioContext) { folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) - command := getCreateCommand(folder.Id, "Library Panel Name") + command := getCreatePanelCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) sc.reqContext.SignedInUser.OrgRole = testCase.role - cmd := patchLibraryPanelCommand{FolderID: -100, Version: 1} + cmd := patchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(Panel)} sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) resp = sc.service.patchHandler(sc.reqContext, cmd) require.Equal(t, 404, resp.Status()) }) } + var getCases = []struct { + role models.RoleType + statuses []int + }{ + {models.ROLE_ADMIN, []int{200, 200, 200, 200, 200, 200, 200}}, + {models.ROLE_EDITOR, []int{200, 404, 200, 200, 200, 200, 200}}, + {models.ROLE_VIEWER, []int{200, 404, 404, 200, 200, 200, 200}}, + } + + for _, testCase := range getCases { + testScenario(t, fmt.Sprintf("When %s tries to get a library panel, it should return correct response", testCase.role), + func(t *testing.T, sc scenarioContext) { + var results []libraryElement + for i, folderCase := range folderCases { + folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase) + cmd := getCreatePanelCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i)) + resp := sc.service.createHandler(sc.reqContext, cmd) + result := validateAndUnMarshalResponse(t, resp) + result.Result.Meta.CreatedBy.Name = userInDbName + result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.Name = userInDbName + result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.FolderName = folder.Title + result.Result.Meta.FolderUID = folder.Uid + results = append(results, result.Result) + } + sc.reqContext.SignedInUser.OrgRole = testCase.role + + for i, result := range results { + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.UID}) + resp := sc.service.getHandler(sc.reqContext) + require.Equal(t, testCase.statuses[i], resp.Status()) + } + }) + + testScenario(t, fmt.Sprintf("When %s tries to get a library panel from General folder, it should return correct response", testCase.role), + func(t *testing.T, sc scenarioContext) { + cmd := getCreatePanelCommand(0, "Library Panel in General Folder") + resp := sc.service.createHandler(sc.reqContext, cmd) + result := validateAndUnMarshalResponse(t, resp) + result.Result.Meta.CreatedBy.Name = userInDbName + result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.Name = userInDbName + result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.FolderName = "General" + result.Result.Meta.FolderUID = "" + sc.reqContext.SignedInUser.OrgRole = testCase.role + + sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) + resp = sc.service.getHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + var actual libraryElementResult + err := json.Unmarshal(resp.Body(), &actual) + require.NoError(t, err) + if diff := cmp.Diff(result.Result, actual.Result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + } + var getAllCases = []struct { role models.RoleType panels int @@ -327,16 +285,16 @@ func TestLibraryPanelPermissions(t *testing.T) { for _, testCase := range getAllCases { testScenario(t, fmt.Sprintf("When %s tries to get all library panels, it should return correct response", testCase.role), func(t *testing.T, sc scenarioContext) { - var results []libraryPanel + var results []libraryElement for i, folderCase := range folderCases { folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase) - cmd := getCreateCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i)) + cmd := getCreatePanelCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i)) resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) - result.Result.Meta.CreatedBy.Name = UserInDbName - result.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar - result.Result.Meta.UpdatedBy.Name = UserInDbName - result.Result.Meta.UpdatedBy.AvatarUrl = UserInDbAvatar + result.Result.Meta.CreatedBy.Name = userInDbName + result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.Name = userInDbName + result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar result.Result.Meta.FolderName = folder.Title result.Result.Meta.FolderUID = folder.Uid results = append(results, result.Result) @@ -345,29 +303,33 @@ func TestLibraryPanelPermissions(t *testing.T) { resp := sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var actual libraryPanelsSearch + var actual libraryElementsSearch err := json.Unmarshal(resp.Body(), &actual) require.NoError(t, err) - require.Equal(t, testCase.panels, len(actual.Result.LibraryPanels)) + require.Equal(t, testCase.panels, len(actual.Result.Elements)) for _, folderIndex := range testCase.folderIndexes { var folderID = int64(folderIndex + 2) // testScenario creates one folder and general folder doesn't count - var foundResult libraryPanel - var actualResult libraryPanel + var foundExists = false + var foundResult libraryElement + var actualExists = false + var actualResult libraryElement for _, result := range results { if result.FolderID == folderID { + foundExists = true foundResult = result break } } - require.NotEmpty(t, foundResult) + require.Equal(t, foundExists, true) - for _, result := range actual.Result.LibraryPanels { + for _, result := range actual.Result.Elements { if result.FolderID == folderID { + actualExists = true actualResult = result break } } - require.NotEmpty(t, actualResult) + require.Equal(t, actualExists, true) if diff := cmp.Diff(foundResult, actualResult, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) @@ -377,107 +339,23 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to get all library panels from General folder, it should return correct response", testCase.role), func(t *testing.T, sc scenarioContext) { - cmd := getCreateCommand(0, "Library Panel in General Folder") + cmd := getCreatePanelCommand(0, "Library Panel in General Folder") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) - result.Result.Meta.CreatedBy.Name = UserInDbName - result.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar - result.Result.Meta.UpdatedBy.Name = UserInDbName - result.Result.Meta.UpdatedBy.AvatarUrl = UserInDbAvatar + result.Result.Meta.CreatedBy.Name = userInDbName + result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.Name = userInDbName + result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar result.Result.Meta.FolderName = "General" sc.reqContext.SignedInUser.OrgRole = testCase.role resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - var actual libraryPanelsSearch + var actual libraryElementsSearch err := json.Unmarshal(resp.Body(), &actual) require.NoError(t, err) - require.Equal(t, 1, len(actual.Result.LibraryPanels)) - if diff := cmp.Diff(result.Result, actual.Result.LibraryPanels[0], getCompareOptions()...); diff != "" { - t.Fatalf("Result mismatch (-want +got):\n%s", diff) - } - }) - - testScenario(t, fmt.Sprintf("When %s tries to get connected dashboards for a library panel, it should return correct connected dashboard IDs", testCase.role), - func(t *testing.T, sc scenarioContext) { - cmd := getCreateCommand(0, "Library Panel in General Folder") - resp := sc.service.createHandler(sc.reqContext, cmd) - result := validateAndUnMarshalResponse(t, resp) - for i, folderCase := range folderCases { - folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase) - dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - } - sc.reqContext.SignedInUser.OrgRole = testCase.role - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) - resp = sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - var dashResult libraryPanelDashboardsResult - err := json.Unmarshal(resp.Body(), &dashResult) - require.NoError(t, err) - require.Equal(t, testCase.panels, len(dashResult.Result)) - }) - } - - var getCases = []struct { - role models.RoleType - statuses []int - }{ - {models.ROLE_ADMIN, []int{200, 200, 200, 200, 200, 200, 200}}, - {models.ROLE_EDITOR, []int{200, 404, 200, 200, 200, 200, 200}}, - {models.ROLE_VIEWER, []int{200, 404, 404, 200, 200, 200, 200}}, - } - - for _, testCase := range getCases { - testScenario(t, fmt.Sprintf("When %s tries to get a library panel, it should return correct response", testCase.role), - func(t *testing.T, sc scenarioContext) { - var results []libraryPanel - for i, folderCase := range folderCases { - folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase) - cmd := getCreateCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i)) - resp := sc.service.createHandler(sc.reqContext, cmd) - result := validateAndUnMarshalResponse(t, resp) - result.Result.Meta.CreatedBy.Name = UserInDbName - result.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar - result.Result.Meta.UpdatedBy.Name = UserInDbName - result.Result.Meta.UpdatedBy.AvatarUrl = UserInDbAvatar - result.Result.Meta.FolderName = folder.Title - result.Result.Meta.FolderUID = folder.Uid - results = append(results, result.Result) - } - sc.reqContext.SignedInUser.OrgRole = testCase.role - - for i, result := range results { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.UID}) - resp := sc.service.getHandler(sc.reqContext) - require.Equal(t, testCase.statuses[i], resp.Status()) - } - }) - - testScenario(t, fmt.Sprintf("When %s tries to get a library panel from General folder, it should return correct response", testCase.role), - func(t *testing.T, sc scenarioContext) { - cmd := getCreateCommand(0, "Library Panel in General Folder") - resp := sc.service.createHandler(sc.reqContext, cmd) - result := validateAndUnMarshalResponse(t, resp) - result.Result.Meta.CreatedBy.Name = UserInDbName - result.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar - result.Result.Meta.UpdatedBy.Name = UserInDbName - result.Result.Meta.UpdatedBy.AvatarUrl = UserInDbAvatar - result.Result.Meta.FolderName = "General" - result.Result.Meta.FolderUID = "" - sc.reqContext.SignedInUser.OrgRole = testCase.role - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) - resp = sc.service.getHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - var actual libraryPanelResult - err := json.Unmarshal(resp.Body(), &actual) - require.NoError(t, err) - if diff := cmp.Diff(result.Result, actual.Result, getCompareOptions()...); diff != "" { + require.Equal(t, 1, len(actual.Result.Elements)) + if diff := cmp.Diff(result.Result, actual.Result.Elements[0], getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } }) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go new file mode 100644 index 00000000000..c1176d28cb3 --- /dev/null +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -0,0 +1,353 @@ +package libraryelements + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + + dboards "github.com/grafana/grafana/pkg/dashboards" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "gopkg.in/macaron.v1" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +const userInDbName = "user_in_db" +const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad" + +func TestDeleteLibraryPanelsInFolder(t *testing.T) { + scenarioWithPanel(t, "When an admin tries to delete a folder that contains connected library elements, it should fail", + func(t *testing.T, sc scenarioContext) { + dashJSON := map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": int64(1), + "gridPos": map[string]interface{}{ + "h": 6, + "w": 6, + "x": 0, + "y": 0, + }, + }, + map[string]interface{}{ + "id": int64(2), + "gridPos": map[string]interface{}{ + "h": 6, + "w": 6, + "x": 6, + "y": 0, + }, + "libraryPanel": map[string]interface{}{ + "uid": sc.initialResult.Result.UID, + "name": sc.initialResult.Result.Name, + }, + }, + }, + } + dash := models.Dashboard{ + Title: "Testing DeleteLibraryElementsInFolder", + Data: simplejson.NewFromAny(dashJSON), + } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id) + require.NoError(t, err) + + err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext, sc.folder.Uid) + require.EqualError(t, err, ErrFolderHasConnectedLibraryElements.Error()) + }) + + scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too", + func(t *testing.T, sc scenarioContext) { + command := getCreateVariableCommand(sc.folder.Id, "query0") + resp := sc.service.createHandler(sc.reqContext, command) + require.Equal(t, 200, resp.Status()) + + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + var result libraryElementsSearch + err := json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + require.NotNil(t, result.Result) + require.Equal(t, 2, len(result.Result.Elements)) + + err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext, sc.folder.Uid) + require.NoError(t, err) + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + err = json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + require.NotNil(t, result.Result) + require.Equal(t, 0, len(result.Result.Elements)) + }) +} + +type libraryElement struct { + ID int64 `json:"id"` + OrgID int64 `json:"orgId"` + FolderID int64 `json:"folderId"` + UID string `json:"uid"` + Name string `json:"name"` + Kind int64 `json:"kind"` + Type string `json:"type"` + Description string `json:"description"` + Model map[string]interface{} `json:"model"` + Version int64 `json:"version"` + Meta LibraryElementDTOMeta `json:"meta"` +} + +type libraryElementResult struct { + Result libraryElement `json:"result"` +} + +type libraryElementsSearch struct { + Result libraryElementsSearchResult `json:"result"` +} + +type libraryElementsSearchResult struct { + TotalCount int64 `json:"totalCount"` + Elements []libraryElement `json:"elements"` + Page int `json:"page"` + PerPage int `json:"perPage"` +} + +func overrideLibraryElementServiceInRegistry(cfg *setting.Cfg) LibraryElementService { + l := LibraryElementService{ + SQLStore: nil, + Cfg: cfg, + } + + overrideServiceFunc := func(d registry.Descriptor) (*registry.Descriptor, bool) { + descriptor := registry.Descriptor{ + Name: "LibraryElementService", + Instance: &l, + InitPriority: 0, + } + + return &descriptor, true + } + + registry.RegisterOverride(overrideServiceFunc) + + return l +} + +func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand { + command := getCreateCommandWithModel(folderID, name, Panel, []byte(` + { + "datasource": "${DS_GDEV-TESTDATA}", + "id": 1, + "title": "Text - Library Panel", + "type": "text", + "description": "A description" + } + `)) + + return command +} + +func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementCommand { + command := getCreateCommandWithModel(folderID, name, Variable, []byte(` + { + "datasource": "${DS_GDEV-TESTDATA}", + "name": "query0", + "type": "query", + "description": "A description" + } + `)) + + return command +} + +func getCreateCommandWithModel(folderID int64, name string, kind LibraryElementKind, model []byte) CreateLibraryElementCommand { + command := CreateLibraryElementCommand{ + FolderID: folderID, + Name: name, + Model: model, + Kind: int64(kind), + } + + return command +} + +type scenarioContext struct { + ctx *macaron.Context + service *LibraryElementService + reqContext *models.ReqContext + user models.SignedInUser + folder *models.Folder + initialResult libraryElementResult + sqlStore *sqlstore.SQLStore +} + +type folderACLItem struct { + roleType models.RoleType + permission models.PermissionType +} + +func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard { + dash.FolderId = folderID + dashItem := &dashboards.SaveDashboardDTO{ + Dashboard: dash, + Message: "", + OrgId: user.OrgId, + User: &user, + Overwrite: false, + } + origUpdateAlerting := dashboards.UpdateAlerting + t.Cleanup(func() { + dashboards.UpdateAlerting = origUpdateAlerting + }) + dashboards.UpdateAlerting = func(store dboards.Store, orgID int64, dashboard *models.Dashboard, + user *models.SignedInUser) error { + return nil + } + + dashboard, err := dashboards.NewService(sqlStore).SaveDashboard(dashItem, true) + require.NoError(t, err) + + return dashboard +} + +func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string, user models.SignedInUser, + items []folderACLItem) *models.Folder { + t.Helper() + + s := dashboards.NewFolderService(user.OrgId, &user, sqlStore) + t.Logf("Creating folder with title and UID %q", title) + folder, err := s.CreateFolder(title, title) + require.NoError(t, err) + + updateFolderACL(t, sqlStore, folder.Id, items) + + return folder +} + +func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, items []folderACLItem) { + t.Helper() + + if len(items) == 0 { + return + } + + var aclItems []*models.DashboardAcl + for _, item := range items { + role := item.roleType + permission := item.permission + aclItems = append(aclItems, &models.DashboardAcl{ + DashboardID: folderID, + Role: &role, + Permission: permission, + Created: time.Now(), + Updated: time.Now(), + }) + } + + err := sqlStore.UpdateDashboardACL(folderID, aclItems) + require.NoError(t, err) +} + +func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryElementResult { + t.Helper() + + require.Equal(t, 200, resp.Status()) + + var result = libraryElementResult{} + err := json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + return result +} + +func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { + t.Helper() + + testScenario(t, desc, func(t *testing.T, sc scenarioContext) { + command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel") + resp := sc.service.createHandler(sc.reqContext, command) + sc.initialResult = validateAndUnMarshalResponse(t, resp) + + fn(t, sc) + }) +} + +// testScenario is a wrapper around t.Run performing common setup for library panel tests. +// It takes your real test function as a callback. +func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { + t.Helper() + + t.Run(desc, func(t *testing.T) { + t.Cleanup(registry.ClearOverrides) + + ctx := macaron.Context{ + Req: macaron.Request{Request: &http.Request{}}, + } + orgID := int64(1) + role := models.ROLE_ADMIN + + cfg := setting.NewCfg() + // Everything in this service is behind the feature toggle "panelLibrary" + cfg.FeatureToggles = map[string]bool{"panelLibrary": true} + // Because the LibraryElementService is behind a feature toggle, we need to override the service in the registry + // with a Cfg that contains the feature toggle so migrations are run properly + service := overrideLibraryElementServiceInRegistry(cfg) + + // We need to assign SQLStore after the override and migrations are done + sqlStore := sqlstore.InitTestDB(t) + service.SQLStore = sqlStore + + user := models.SignedInUser{ + UserId: 1, + Name: "Signed In User", + Login: "signed_in_user", + Email: "signed.in.user@test.com", + OrgId: orgID, + OrgRole: role, + LastSeenAt: time.Now(), + } + + // deliberate difference between signed in user and user in db to make it crystal clear + // what to expect in the tests + // In the real world these are identical + cmd := models.CreateUserCommand{ + Email: "user.in.db@test.com", + Name: "User In DB", + Login: userInDbName, + } + _, err := sqlStore.CreateUser(context.Background(), cmd) + require.NoError(t, err) + + sc := scenarioContext{ + user: user, + ctx: &ctx, + service: &service, + sqlStore: sqlStore, + reqContext: &models.ReqContext{ + Context: &ctx, + SignedInUser: &user, + }, + } + + sc.folder = createFolderWithACL(t, sc.sqlStore, "ScenarioFolder", sc.user, []folderACLItem{}) + + 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/libraryelements/models.go b/pkg/services/libraryelements/models.go new file mode 100644 index 00000000000..50f39027696 --- /dev/null +++ b/pkg/services/libraryelements/models.go @@ -0,0 +1,190 @@ +package libraryelements + +import ( + "encoding/json" + "errors" + "time" +) + +type LibraryElementKind int + +const ( + Panel LibraryElementKind = iota + 1 + Variable +) + +type LibraryConnectionKind int + +const ( + Dashboard LibraryConnectionKind = iota + 1 +) + +// LibraryElement is the model for library element definitions. +type LibraryElement struct { + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` + FolderID int64 `xorm:"folder_id"` + UID string `xorm:"uid"` + Name string + Kind int64 + Type string + Description string + Model json.RawMessage + Version int64 + + Created time.Time + Updated time.Time + + CreatedBy int64 + UpdatedBy int64 +} + +// LibraryElementWithMeta is the model used to retrieve entities with additional meta information. +type LibraryElementWithMeta struct { + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` + FolderID int64 `xorm:"folder_id"` + UID string `xorm:"uid"` + Name string + Kind int64 + Type string + Description string + Model json.RawMessage + Version int64 + + Created time.Time + Updated time.Time + + FolderName string + FolderUID string `xorm:"folder_uid"` + Connections int64 + CreatedBy int64 + UpdatedBy int64 + CreatedByName string + CreatedByEmail string + UpdatedByName string + UpdatedByEmail string +} + +// LibraryElementDTO is the frontend DTO for entities. +type LibraryElementDTO struct { + ID int64 `json:"id"` + OrgID int64 `json:"orgId"` + FolderID int64 `json:"folderId"` + UID string `json:"uid"` + Name string `json:"name"` + Kind int64 `json:"kind"` + Type string `json:"type"` + Description string `json:"description"` + Model json.RawMessage `json:"model"` + Version int64 `json:"version"` + Meta LibraryElementDTOMeta `json:"meta"` +} + +// LibraryElementSearchResult is the search result for entities. +type LibraryElementSearchResult struct { + TotalCount int64 `json:"totalCount"` + Elements []LibraryElementDTO `json:"elements"` + Page int `json:"page"` + PerPage int `json:"perPage"` +} + +// LibraryElementDTOMeta is the meta information for LibraryElementDTO. +type LibraryElementDTOMeta struct { + FolderName string `json:"folderName"` + FolderUID string `json:"folderUid"` + Connections int64 `json:"connections"` + + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + + CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` + UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"` +} + +// LibraryElementDTOMetaUser is the meta information for user that creates/changes the library element. +type LibraryElementDTOMetaUser struct { + ID int64 `json:"id"` + Name string `json:"name"` + AvatarURL string `json:"avatarUrl"` +} + +// libraryElementConnection is the model for library element connections. +type libraryElementConnection struct { + ID int64 `xorm:"pk autoincr 'id'"` + LibraryElementID int64 `xorm:"library_element_id"` + ConnectionKind int64 `xorm:"connection_kind"` + ConnectionID int64 `xorm:"connection_id"` + Created time.Time + CreatedBy int64 +} + +// libraryElementConnectionWithMeta is the model for library element connections with meta. +type libraryElementConnectionWithMeta struct { + ID int64 `xorm:"pk autoincr 'id'"` + LibraryElementID int64 `xorm:"library_element_id"` + ConnectionKind int64 `xorm:"connection_kind"` + ConnectionID int64 `xorm:"connection_id"` + Created time.Time + CreatedBy int64 + CreatedByName string + CreatedByEmail string +} + +// LibraryElementConnectionDTO is the frontend DTO for element connections. +type LibraryElementConnectionDTO struct { + ID int64 `json:"id"` + Kind int64 `json:"kind"` + ElementID int64 `json:"elementId"` + ConnectionID int64 `json:"connectionId"` + Created time.Time `json:"created"` + CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` +} + +var ( + // errLibraryElementAlreadyExists is an error for when the user tries to add a library element that already exists. + errLibraryElementAlreadyExists = errors.New("library element with that name already exists") + // errLibraryElementNotFound is an error for when a library element can't be found. + errLibraryElementNotFound = errors.New("library element could not be found") + // errLibraryElementDashboardNotFound is an error for when a library element connection can't be found. + errLibraryElementDashboardNotFound = errors.New("library element connection could not be found") + // errLibraryElementHasConnections is an error for when an user deletes a library element that is connected. + errLibraryElementHasConnections = errors.New("the library element has connections") + // errLibraryElementVersionMismatch is an error for when a library element has been changed by someone else. + errLibraryElementVersionMismatch = errors.New("the library element has been changed by someone else") + // errLibraryElementUnSupportedElementKind is an error for when the kind is unsupported. + errLibraryElementUnSupportedElementKind = errors.New("the element kind is not supported") + // ErrFolderHasConnectedLibraryElements is an error for when an user deletes a folder that contains connected library elements. + ErrFolderHasConnectedLibraryElements = errors.New("folder contains library elements that are linked in use") +) + +// Commands + +// CreateLibraryElementCommand is the command for adding a LibraryElement +type CreateLibraryElementCommand struct { + FolderID int64 `json:"folderId"` + Name string `json:"name"` + Model json.RawMessage `json:"model"` + Kind int64 `json:"kind" binding:"Required"` +} + +// patchLibraryElementCommand is the command for patching a LibraryElement +type patchLibraryElementCommand struct { + FolderID int64 `json:"folderId" binding:"Default(-1)"` + Name string `json:"name"` + Model json.RawMessage `json:"model"` + Kind int64 `json:"kind" binding:"Required"` + Version int64 `json:"version" binding:"Required"` +} + +// searchLibraryElementsQuery is the query used for searching for Elements +type searchLibraryElementsQuery struct { + perPage int + page int + searchString string + sortDirection string + kind int + typeFilter string + excludeUID string + folderFilter string +} diff --git a/pkg/services/librarypanels/writers.go b/pkg/services/libraryelements/writers.go similarity index 62% rename from pkg/services/librarypanels/writers.go rename to pkg/services/libraryelements/writers.go index 0070438ab26..6ad531ef774 100644 --- a/pkg/services/librarypanels/writers.go +++ b/pkg/services/libraryelements/writers.go @@ -1,4 +1,4 @@ -package librarypanels +package libraryelements import ( "bytes" @@ -8,35 +8,41 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" ) -func writePerPageSQL(query searchLibraryPanelsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) { +func writePerPageSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) { if query.perPage != 0 { offset := query.perPage * (query.page - 1) builder.Write(sqlStore.Dialect.LimitOffset(int64(query.perPage), int64(offset))) } } -func writePanelFilterSQL(panelFilter []string, builder *sqlstore.SQLBuilder) { - if len(panelFilter) > 0 { +func writeKindSQL(query searchLibraryElementsQuery, builder *sqlstore.SQLBuilder) { + if LibraryElementKind(query.kind) == Panel || LibraryElementKind(query.kind) == Variable { + builder.Write(" AND le.kind = ?", query.kind) + } +} + +func writeTypeFilterSQL(typeFilter []string, builder *sqlstore.SQLBuilder) { + if len(typeFilter) > 0 { var sql bytes.Buffer params := make([]interface{}, 0) - sql.WriteString(` AND lp.type IN (?` + strings.Repeat(",?", len(panelFilter)-1) + ")") - for _, filter := range panelFilter { + sql.WriteString(` AND le.type IN (?` + strings.Repeat(",?", len(typeFilter)-1) + ")") + for _, filter := range typeFilter { params = append(params, filter) } builder.Write(sql.String(), params...) } } -func writeSearchStringSQL(query searchLibraryPanelsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) { +func writeSearchStringSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) { if len(strings.TrimSpace(query.searchString)) > 0 { - builder.Write(" AND (lp.name "+sqlStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%") - builder.Write(" OR lp.description "+sqlStore.Dialect.LikeStr()+" ?)", "%"+query.searchString+"%") + builder.Write(" AND (le.name "+sqlStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%") + builder.Write(" OR le.description "+sqlStore.Dialect.LikeStr()+" ?)", "%"+query.searchString+"%") } } -func writeExcludeSQL(query searchLibraryPanelsQuery, builder *sqlstore.SQLBuilder) { +func writeExcludeSQL(query searchLibraryElementsQuery, builder *sqlstore.SQLBuilder) { if len(strings.TrimSpace(query.excludeUID)) > 0 { - builder.Write(" AND lp.uid <> ?", query.excludeUID) + builder.Write(" AND le.uid <> ?", query.excludeUID) } } @@ -46,8 +52,8 @@ type FolderFilter struct { parseError error } -func parseFolderFilter(query searchLibraryPanelsQuery) FolderFilter { - var folderIDs []string +func parseFolderFilter(query searchLibraryElementsQuery) FolderFilter { + folderIDs := make([]string, 0) if len(strings.TrimSpace(query.folderFilter)) == 0 { return FolderFilter{ includeGeneralFolder: true, @@ -94,7 +100,7 @@ func (f *FolderFilter) writeFolderFilterSQL(includeGeneral bool, builder *sqlsto params = append(params, filter) } if len(params) > 0 { - sql.WriteString(` AND lp.folder_id IN (?` + strings.Repeat(",?", len(params)-1) + ")") + sql.WriteString(` AND le.folder_id IN (?` + strings.Repeat(",?", len(params)-1) + ")") builder.Write(sql.String(), params...) } diff --git a/pkg/services/librarypanels/api.go b/pkg/services/librarypanels/api.go deleted file mode 100644 index feaeffe1bae..00000000000 --- a/pkg/services/librarypanels/api.go +++ /dev/null @@ -1,144 +0,0 @@ -package librarypanels - -import ( - "errors" - - "github.com/go-macaron/binding" - - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/util" -) - -func (lps *LibraryPanelService) registerAPIEndpoints() { - if !lps.IsEnabled() { - return - } - - lps.RouteRegister.Group("/api/library-panels", func(libraryPanels routing.RouteRegister) { - libraryPanels.Post("/", middleware.ReqSignedIn, binding.Bind(createLibraryPanelCommand{}), routing.Wrap(lps.createHandler)) - libraryPanels.Post("/:uid/dashboards/:dashboardId", middleware.ReqSignedIn, routing.Wrap(lps.connectHandler)) - libraryPanels.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(lps.deleteHandler)) - libraryPanels.Delete("/:uid/dashboards/:dashboardId", middleware.ReqSignedIn, routing.Wrap(lps.disconnectHandler)) - libraryPanels.Get("/", middleware.ReqSignedIn, routing.Wrap(lps.getAllHandler)) - libraryPanels.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(lps.getHandler)) - libraryPanels.Get("/:uid/dashboards/", middleware.ReqSignedIn, routing.Wrap(lps.getConnectedDashboardsHandler)) - libraryPanels.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryPanelCommand{}), routing.Wrap(lps.patchHandler)) - }) -} - -// createHandler handles POST /api/library-panels. -func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd createLibraryPanelCommand) response.Response { - panel, err := lps.createLibraryPanel(c, cmd) - if err != nil { - return toLibraryPanelError(err, "Failed to create library panel") - } - - return response.JSON(200, util.DynMap{"result": panel}) -} - -// connectHandler handles POST /api/library-panels/:uid/dashboards/:dashboardId. -func (lps *LibraryPanelService) connectHandler(c *models.ReqContext) response.Response { - 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") -} - -// deleteHandler handles DELETE /api/library-panels/:uid. -func (lps *LibraryPanelService) deleteHandler(c *models.ReqContext) response.Response { - err := lps.deleteLibraryPanel(c, c.Params(":uid")) - if err != nil { - return toLibraryPanelError(err, "Failed to delete library panel") - } - - return response.Success("Library panel deleted") -} - -// disconnectHandler handles DELETE /api/library-panels/:uid/dashboards/:dashboardId. -func (lps *LibraryPanelService) disconnectHandler(c *models.ReqContext) response.Response { - err := lps.disconnectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId")) - if err != nil { - return toLibraryPanelError(err, "Failed to disconnect library panel") - } - - return response.Success("Library panel disconnected") -} - -// getHandler handles GET /api/library-panels/:uid. -func (lps *LibraryPanelService) getHandler(c *models.ReqContext) response.Response { - libraryPanel, err := lps.getLibraryPanel(c, c.Params(":uid")) - if err != nil { - return toLibraryPanelError(err, "Failed to get library panel") - } - - return response.JSON(200, util.DynMap{"result": libraryPanel}) -} - -// getAllHandler handles GET /api/library-panels/. -func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Response { - query := searchLibraryPanelsQuery{ - perPage: c.QueryInt("perPage"), - page: c.QueryInt("page"), - searchString: c.Query("searchString"), - sortDirection: c.Query("sortDirection"), - panelFilter: c.Query("panelFilter"), - excludeUID: c.Query("excludeUid"), - folderFilter: c.Query("folderFilter"), - } - libraryPanels, err := lps.getAllLibraryPanels(c, query) - if err != nil { - return toLibraryPanelError(err, "Failed to get library panels") - } - - return response.JSON(200, util.DynMap{"result": libraryPanels}) -} - -// getConnectedDashboardsHandler handles GET /api/library-panels/:uid/dashboards/. -func (lps *LibraryPanelService) getConnectedDashboardsHandler(c *models.ReqContext) response.Response { - dashboardIDs, err := lps.getConnectedDashboards(c, c.Params(":uid")) - if err != nil { - return toLibraryPanelError(err, "Failed to get connected dashboards") - } - - return response.JSON(200, util.DynMap{"result": dashboardIDs}) -} - -// patchHandler handles PATCH /api/library-panels/:uid -func (lps *LibraryPanelService) patchHandler(c *models.ReqContext, cmd patchLibraryPanelCommand) response.Response { - libraryPanel, err := lps.patchLibraryPanel(c, cmd, c.Params(":uid")) - if err != nil { - 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) - } - if errors.Is(err, errLibraryPanelHasConnectedDashboards) { - return response.Error(403, errLibraryPanelHasConnectedDashboards.Error(), err) - } - return response.Error(500, message, err) -} diff --git a/pkg/services/librarypanels/database.go b/pkg/services/librarypanels/database.go deleted file mode 100644 index fc072e80d12..00000000000 --- a/pkg/services/librarypanels/database.go +++ /dev/null @@ -1,702 +0,0 @@ -package librarypanels - -import ( - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/search" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/util" -) - -var ( - selectLibrayPanelDTOWithMeta = ` -SELECT DISTINCT - lp.name, lp.id, lp.org_id, lp.folder_id, lp.uid, lp.type, lp.description, 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 - , u2.login AS updated_by_name - , u2.email AS updated_by_email - , (SELECT COUNT(dashboard_id) FROM library_panel_dashboard WHERE librarypanel_id = lp.id) AS connected_dashboards -` - fromLibrayPanelDTOWithMeta = ` -FROM library_panel AS lp - LEFT JOIN user AS u1 ON lp.created_by = u1.id - LEFT JOIN user AS u2 ON lp.updated_by = u2.id -` - sqlStatmentLibrayPanelDTOWithMeta = selectLibrayPanelDTOWithMeta + fromLibrayPanelDTOWithMeta -) - -func syncFieldsWithModel(libraryPanel *LibraryPanel) error { - var model map[string]interface{} - if err := json.Unmarshal(libraryPanel.Model, &model); err != nil { - return err - } - - model["title"] = libraryPanel.Name - if model["type"] != nil { - libraryPanel.Type = model["type"].(string) - } else { - model["type"] = libraryPanel.Type - } - if model["description"] != nil { - libraryPanel.Description = model["description"].(string) - } else { - model["description"] = libraryPanel.Description - } - syncedModel, err := json.Marshal(&model) - if err != nil { - return err - } - - libraryPanel.Model = syncedModel - - return nil -} - -// createLibraryPanel adds a Library Panel. -func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd createLibraryPanelCommand) (LibraryPanelDTO, error) { - libraryPanel := LibraryPanel{ - OrgID: c.SignedInUser.OrgId, - FolderID: cmd.FolderID, - UID: util.GenerateShortUID(), - Name: cmd.Name, - Model: cmd.Model, - Version: 1, - - Created: time.Now(), - Updated: time.Now(), - - CreatedBy: c.SignedInUser.UserId, - UpdatedBy: c.SignedInUser.UserId, - } - - if err := syncFieldsWithModel(&libraryPanel); err != nil { - return LibraryPanelDTO{}, err - } - - err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - if err := lps.requirePermissionsOnFolder(c.SignedInUser, cmd.FolderID); err != nil { - return err - } - if _, err := session.Insert(&libraryPanel); err != nil { - if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) { - return errLibraryPanelAlreadyExists - } - return err - } - return nil - }) - - dto := LibraryPanelDTO{ - ID: libraryPanel.ID, - OrgID: libraryPanel.OrgID, - FolderID: libraryPanel.FolderID, - UID: libraryPanel.UID, - Name: libraryPanel.Name, - Type: libraryPanel.Type, - Description: libraryPanel.Description, - Model: libraryPanel.Model, - Version: libraryPanel.Version, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - ConnectedDashboards: 0, - Created: libraryPanel.Created, - Updated: libraryPanel.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ - ID: libraryPanel.CreatedBy, - Name: c.SignedInUser.Login, - AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email), - }, - UpdatedBy: LibraryPanelDTOMetaUser{ - ID: libraryPanel.UpdatedBy, - Name: c.SignedInUser.Login, - AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email), - }, - }, - } - - return dto, 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(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - return lps.internalConnectDashboard(session, c.SignedInUser, uid, dashboardID) - }) - - return err -} - -func (lps *LibraryPanelService) internalConnectDashboard(session *sqlstore.DBSession, user *models.SignedInUser, - uid string, dashboardID int64) error { - panel, err := getLibraryPanel(session, uid, user.OrgId) - if err != nil { - return err - } - if err := lps.requirePermissionsOnFolder(user, panel.FolderID); err != nil { - return err - } - - libraryPanelDashboard := libraryPanelDashboard{ - DashboardID: dashboardID, - LibraryPanelID: panel.ID, - Created: time.Now(), - CreatedBy: user.UserId, - } - if _, err := session.Insert(&libraryPanelDashboard); err != nil { - if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) { - return nil - } - return err - } - return nil -} - -// connectLibraryPanelsForDashboard adds connections for all Library Panels in a Dashboard. -func (lps *LibraryPanelService) connectLibraryPanelsForDashboard(c *models.ReqContext, uids []string, dashboardID int64) error { - err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - _, err := session.Exec("DELETE FROM library_panel_dashboard WHERE dashboard_id=?", dashboardID) - if err != nil { - return err - } - for _, uid := range uids { - err := lps.internalConnectDashboard(session, c.SignedInUser, uid, dashboardID) - if err != nil { - return err - } - } - return nil - }) - - return err -} - -// deleteLibraryPanel deletes a Library Panel. -func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid string) error { - return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) - if err != nil { - return err - } - if err := lps.requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil { - return err - } - var dashIDs []struct { - DashboardID int64 `xorm:"dashboard_id"` - } - sql := "SELECT dashboard_id FROM library_panel_dashboard WHERE librarypanel_id=?" - if err := session.SQL(sql, panel.ID).Find(&dashIDs); err != nil { - return err - } else if len(dashIDs) > 0 { - return errLibraryPanelHasConnectedDashboards - } - - result, err := session.Exec("DELETE FROM library_panel WHERE id=?", panel.ID) - if err != nil { - return err - } - if rowsAffected, err := result.RowsAffected(); err != nil { - return err - } else if rowsAffected != 1 { - return errLibraryPanelNotFound - } - - return nil - }) -} - -// 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(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) - if err != nil { - return err - } - if err := lps.requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); 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 - }) -} - -// disconnectLibraryPanelsForDashboard deletes connections for all Library Panels in a Dashboard. -func (lps *LibraryPanelService) disconnectLibraryPanelsForDashboard(c *models.ReqContext, dashboardID int64, panelCount int64) error { - return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - result, err := session.Exec("DELETE FROM library_panel_dashboard WHERE dashboard_id=?", dashboardID) - if err != nil { - return err - } - if rowsAffected, err := result.RowsAffected(); err != nil { - return err - } else if rowsAffected != panelCount { - lps.log.Warn("Number of disconnects does not match number of panels", "dashboard", dashboardID, "rowsAffected", rowsAffected, "panelCount", panelCount) - } - - return nil - }) -} - -// deleteLibraryPanelsInFolder deletes all Library Panels for a folder. -func (lps *LibraryPanelService) deleteLibraryPanelsInFolder(c *models.ReqContext, folderUID string) error { - return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - var folderUIDs []struct { - ID int64 `xorm:"id"` - } - err := session.SQL("SELECT id from dashboard WHERE uid=? AND org_id=? AND is_folder=1", folderUID, c.SignedInUser.OrgId).Find(&folderUIDs) - if err != nil { - return err - } - if len(folderUIDs) != 1 { - return fmt.Errorf("found %d folders, while expecting at most one", len(folderUIDs)) - } - folderID := folderUIDs[0].ID - - if err := lps.requirePermissionsOnFolder(c.SignedInUser, folderID); err != nil { - return err - } - var dashIDs []struct { - DashboardID int64 `xorm:"dashboard_id"` - } - sql := "SELECT lpd.dashboard_id FROM library_panel AS lp" - sql += " INNER JOIN library_panel_dashboard lpd on lp.id = lpd.librarypanel_id" - sql += " WHERE lp.folder_id=? AND lp.org_id=?" - err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&dashIDs) - if err != nil { - return err - } - if len(dashIDs) > 0 { - return ErrFolderHasConnectedLibraryPanels - } - - var panelIDs []struct { - ID int64 `xorm:"id"` - } - err = session.SQL("SELECT id from library_panel WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId).Find(&panelIDs) - if err != nil { - return err - } - for _, panelID := range panelIDs { - _, err := session.Exec("DELETE FROM library_panel_dashboard WHERE librarypanel_id=?", panelID.ID) - if err != nil { - return err - } - } - if _, err := session.Exec("DELETE FROM library_panel WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId); err != nil { - return err - } - - return nil - }) -} - -func getLibraryPanel(session *sqlstore.DBSession, uid string, orgID int64) (LibraryPanelWithMeta, error) { - libraryPanels := make([]LibraryPanelWithMeta, 0) - sql := sqlStatmentLibrayPanelDTOWithMeta + "WHERE lp.uid=? AND lp.org_id=?" - sess := session.SQL(sql, uid, orgID) - err := sess.Find(&libraryPanels) - if err != nil { - return LibraryPanelWithMeta{}, err - } - if len(libraryPanels) == 0 { - return LibraryPanelWithMeta{}, errLibraryPanelNotFound - } - if len(libraryPanels) > 1 { - return LibraryPanelWithMeta{}, 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) (LibraryPanelDTO, error) { - var libraryPanel LibraryPanelWithMeta - err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - libraryPanels := make([]LibraryPanelWithMeta, 0) - builder := sqlstore.SQLBuilder{} - builder.Write(selectLibrayPanelDTOWithMeta) - builder.Write(", 'General' as folder_name ") - builder.Write(", '' as folder_uid ") - builder.Write(fromLibrayPanelDTOWithMeta) - builder.Write(` WHERE lp.uid=? AND lp.org_id=? AND lp.folder_id=0`, uid, c.SignedInUser.OrgId) - builder.Write(" UNION ") - builder.Write(selectLibrayPanelDTOWithMeta) - builder.Write(", dashboard.title as folder_name ") - builder.Write(", dashboard.uid as folder_uid ") - builder.Write(fromLibrayPanelDTOWithMeta) - builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id <> 0") - builder.Write(` WHERE lp.uid=? AND lp.org_id=?`, uid, c.SignedInUser.OrgId) - if c.SignedInUser.OrgRole != models.ROLE_ADMIN { - builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW) - } - builder.Write(` OR dashboard.id=0`) - if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); 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 - }) - - dto := LibraryPanelDTO{ - ID: libraryPanel.ID, - OrgID: libraryPanel.OrgID, - FolderID: libraryPanel.FolderID, - UID: libraryPanel.UID, - Name: libraryPanel.Name, - Type: libraryPanel.Type, - Description: libraryPanel.Description, - Model: libraryPanel.Model, - Version: libraryPanel.Version, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: libraryPanel.FolderName, - FolderUID: libraryPanel.FolderUID, - ConnectedDashboards: libraryPanel.ConnectedDashboards, - Created: libraryPanel.Created, - Updated: libraryPanel.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ - ID: libraryPanel.CreatedBy, - Name: libraryPanel.CreatedByName, - AvatarUrl: dtos.GetGravatarUrl(libraryPanel.CreatedByEmail), - }, - UpdatedBy: LibraryPanelDTOMetaUser{ - ID: libraryPanel.UpdatedBy, - Name: libraryPanel.UpdatedByName, - AvatarUrl: dtos.GetGravatarUrl(libraryPanel.UpdatedByEmail), - }, - }, - } - - return dto, err -} - -// getAllLibraryPanels gets all library panels. -func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, query searchLibraryPanelsQuery) (LibraryPanelSearchResult, error) { - libraryPanels := make([]LibraryPanelWithMeta, 0) - result := LibraryPanelSearchResult{} - if query.perPage <= 0 { - query.perPage = 100 - } - if query.page <= 0 { - query.page = 1 - } - var panelFilter []string - if len(strings.TrimSpace(query.panelFilter)) > 0 { - panelFilter = strings.Split(query.panelFilter, ",") - } - folderFilter := parseFolderFilter(query) - if folderFilter.parseError != nil { - return LibraryPanelSearchResult{}, folderFilter.parseError - } - err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - builder := sqlstore.SQLBuilder{} - if folderFilter.includeGeneralFolder { - builder.Write(selectLibrayPanelDTOWithMeta) - builder.Write(", 'General' as folder_name ") - builder.Write(", '' as folder_uid ") - builder.Write(fromLibrayPanelDTOWithMeta) - builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId) - writeSearchStringSQL(query, lps.SQLStore, &builder) - writeExcludeSQL(query, &builder) - writePanelFilterSQL(panelFilter, &builder) - builder.Write(" UNION ") - } - builder.Write(selectLibrayPanelDTOWithMeta) - builder.Write(", dashboard.title as folder_name ") - builder.Write(", dashboard.uid as folder_uid ") - builder.Write(fromLibrayPanelDTOWithMeta) - builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id<>0") - builder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId) - writeSearchStringSQL(query, lps.SQLStore, &builder) - writeExcludeSQL(query, &builder) - writePanelFilterSQL(panelFilter, &builder) - if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil { - return err - } - if c.SignedInUser.OrgRole != models.ROLE_ADMIN { - builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW) - } - if query.sortDirection == search.SortAlphaDesc.Name { - builder.Write(" ORDER BY 1 DESC") - } else { - builder.Write(" ORDER BY 1 ASC") - } - writePerPageSQL(query, lps.SQLStore, &builder) - if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil { - return err - } - - retDTOs := make([]LibraryPanelDTO, 0) - for _, panel := range libraryPanels { - retDTOs = append(retDTOs, LibraryPanelDTO{ - ID: panel.ID, - OrgID: panel.OrgID, - FolderID: panel.FolderID, - UID: panel.UID, - Name: panel.Name, - Type: panel.Type, - Description: panel.Description, - Model: panel.Model, - Version: panel.Version, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: panel.FolderName, - FolderUID: panel.FolderUID, - ConnectedDashboards: panel.ConnectedDashboards, - Created: panel.Created, - Updated: panel.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ - ID: panel.CreatedBy, - Name: panel.CreatedByName, - AvatarUrl: dtos.GetGravatarUrl(panel.CreatedByEmail), - }, - UpdatedBy: LibraryPanelDTOMetaUser{ - ID: panel.UpdatedBy, - Name: panel.UpdatedByName, - AvatarUrl: dtos.GetGravatarUrl(panel.UpdatedByEmail), - }, - }, - }) - } - - var panels []LibraryPanel - countBuilder := sqlstore.SQLBuilder{} - countBuilder.Write("SELECT * FROM library_panel AS lp") - countBuilder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId) - writeSearchStringSQL(query, lps.SQLStore, &countBuilder) - writeExcludeSQL(query, &countBuilder) - writePanelFilterSQL(panelFilter, &countBuilder) - if err := folderFilter.writeFolderFilterSQL(true, &countBuilder); err != nil { - return err - } - if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&panels); err != nil { - return err - } - - result = LibraryPanelSearchResult{ - TotalCount: int64(len(panels)), - LibraryPanels: retDTOs, - Page: query.page, - PerPage: query.perPage, - } - - return nil - }) - - return result, 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(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) - if err != nil { - return err - } - var libraryPanelDashboards []libraryPanelDashboard - builder := sqlstore.SQLBuilder{} - builder.Write("SELECT lpd.* FROM library_panel_dashboard lpd") - builder.Write(" INNER JOIN dashboard AS dashboard on lpd.dashboard_id = dashboard.id") - builder.Write(` WHERE lpd.librarypanel_id=?`, panel.ID) - if c.SignedInUser.OrgRole != models.ROLE_ADMIN { - builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW) - } - if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanelDashboards); err != nil { - return err - } - - for _, lpd := range libraryPanelDashboards { - connectedDashboardIDs = append(connectedDashboardIDs, lpd.DashboardID) - } - - return nil - }) - - return connectedDashboardIDs, err -} - -func (lps *LibraryPanelService) getLibraryPanelsForDashboardID(c *models.ReqContext, dashboardID int64) (map[string]LibraryPanelDTO, error) { - libraryPanelMap := make(map[string]LibraryPanelDTO) - err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - var libraryPanels []LibraryPanelWithMeta - sql := selectLibrayPanelDTOWithMeta + ", coalesce(dashboard.title, 'General') AS folder_name, coalesce(dashboard.uid, '') AS folder_uid " + fromLibrayPanelDTOWithMeta + ` -LEFT JOIN dashboard AS dashboard ON dashboard.id = lp.folder_id AND dashboard.id=? -INNER JOIN library_panel_dashboard AS lpd ON lpd.librarypanel_id = lp.id AND lpd.dashboard_id=? -` - - sess := session.SQL(sql, dashboardID, dashboardID) - err := sess.Find(&libraryPanels) - if err != nil { - return err - } - - for _, panel := range libraryPanels { - libraryPanelMap[panel.UID] = LibraryPanelDTO{ - ID: panel.ID, - OrgID: panel.OrgID, - FolderID: panel.FolderID, - UID: panel.UID, - Name: panel.Name, - Type: panel.Type, - Description: panel.Description, - Model: panel.Model, - Version: panel.Version, - Meta: LibraryPanelDTOMeta{ - CanEdit: panel.CanEdit, - FolderName: panel.FolderName, - FolderUID: panel.FolderUID, - ConnectedDashboards: panel.ConnectedDashboards, - Created: panel.Created, - Updated: panel.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ - ID: panel.CreatedBy, - Name: panel.CreatedByName, - AvatarUrl: dtos.GetGravatarUrl(panel.CreatedByEmail), - }, - UpdatedBy: LibraryPanelDTOMetaUser{ - ID: panel.UpdatedBy, - Name: panel.UpdatedByName, - AvatarUrl: dtos.GetGravatarUrl(panel.UpdatedByEmail), - }, - }, - } - } - - return nil - }) - - return libraryPanelMap, err -} - -func (lps *LibraryPanelService) handleFolderIDPatches(panelToPatch *LibraryPanel, fromFolderID int64, - toFolderID int64, user *models.SignedInUser) error { - // FolderID was not provided in the PATCH request - if toFolderID == -1 { - toFolderID = fromFolderID - } - - // FolderID was provided in the PATCH request - if toFolderID != -1 && toFolderID != fromFolderID { - if err := lps.requirePermissionsOnFolder(user, toFolderID); err != nil { - return err - } - } - - // Always check permissions for the folder where library panel resides - if err := lps.requirePermissionsOnFolder(user, fromFolderID); err != nil { - return err - } - - panelToPatch.FolderID = toFolderID - - return nil -} - -// patchLibraryPanel updates a Library Panel. -func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patchLibraryPanelCommand, uid string) (LibraryPanelDTO, error) { - var dto LibraryPanelDTO - err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - panelInDB, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) - if err != nil { - return err - } - if panelInDB.Version != cmd.Version { - return errLibraryPanelVersionMismatch - } - - var libraryPanel = LibraryPanel{ - ID: panelInDB.ID, - OrgID: c.SignedInUser.OrgId, - FolderID: cmd.FolderID, - UID: uid, - Name: cmd.Name, - Type: panelInDB.Type, - Description: panelInDB.Description, - Model: cmd.Model, - Version: panelInDB.Version + 1, - Created: panelInDB.Created, - CreatedBy: panelInDB.CreatedBy, - Updated: time.Now(), - UpdatedBy: c.SignedInUser.UserId, - } - - if cmd.Name == "" { - libraryPanel.Name = panelInDB.Name - } - if cmd.Model == nil { - libraryPanel.Model = panelInDB.Model - } - if err := lps.handleFolderIDPatches(&libraryPanel, panelInDB.FolderID, cmd.FolderID, c.SignedInUser); err != nil { - return err - } - if err := syncFieldsWithModel(&libraryPanel); err != nil { - return err - } - 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 - } - - dto = LibraryPanelDTO{ - ID: libraryPanel.ID, - OrgID: libraryPanel.OrgID, - FolderID: libraryPanel.FolderID, - UID: libraryPanel.UID, - Name: libraryPanel.Name, - Type: libraryPanel.Type, - Description: libraryPanel.Description, - Model: libraryPanel.Model, - Version: libraryPanel.Version, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - ConnectedDashboards: panelInDB.ConnectedDashboards, - Created: libraryPanel.Created, - Updated: libraryPanel.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ - ID: panelInDB.CreatedBy, - Name: panelInDB.CreatedByName, - AvatarUrl: dtos.GetGravatarUrl(panelInDB.CreatedByEmail), - }, - UpdatedBy: LibraryPanelDTOMetaUser{ - ID: libraryPanel.UpdatedBy, - Name: c.SignedInUser.Login, - AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email), - }, - }, - } - - return nil - }) - - return dto, err -} diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 7fa9be03c52..b86e71908ee 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -8,17 +8,18 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" ) // LibraryPanelService is the service for the Panel Library feature. type LibraryPanelService struct { - Cfg *setting.Cfg `inject:""` - SQLStore *sqlstore.SQLStore `inject:""` - RouteRegister routing.RouteRegister `inject:""` - log log.Logger + Cfg *setting.Cfg `inject:""` + SQLStore *sqlstore.SQLStore `inject:""` + RouteRegister routing.RouteRegister `inject:""` + LibraryElementService *libraryelements.LibraryElementService `inject:""` + log log.Logger } func init() { @@ -27,10 +28,7 @@ func init() { // Init initializes the LibraryPanel service func (lps *LibraryPanelService) Init() error { - lps.log = log.New("librarypanels") - - lps.registerAPIEndpoints() - + lps.log = log.New("library-panels") return nil } @@ -50,7 +48,7 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte return nil } - libraryPanels, err := lps.getLibraryPanelsForDashboardID(c, dash.Id) + elements, err := lps.LibraryElementService.GetElementsForDashboard(c, dash.Id) if err != nil { return err } @@ -69,7 +67,7 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte return errLibraryPanelHeaderUIDMissing } - libraryPanelInDB, ok := libraryPanels[uid] + elementInDB, ok := elements[uid] if !ok { name := libraryPanel.Get("name").MustString() elem := dash.Data.Get("panels").GetIndex(i) @@ -83,8 +81,12 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte continue } + if libraryelements.LibraryElementKind(elementInDB.Kind) != libraryelements.Panel { + continue + } + // we have a match between what is stored in db and in dashboard json - libraryPanelModel, err := libraryPanelInDB.Model.MarshalJSON() + libraryPanelModel, err := elementInDB.Model.MarshalJSON() if err != nil { return fmt.Errorf("could not marshal library panel JSON: %w", err) } @@ -102,27 +104,26 @@ 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, - "type": libraryPanelInDB.Type, - "description": libraryPanelInDB.Description, - "version": libraryPanelInDB.Version, + "uid": elementInDB.UID, + "name": elementInDB.Name, + "type": elementInDB.Type, + "description": elementInDB.Description, + "version": elementInDB.Version, "meta": map[string]interface{}{ - "canEdit": libraryPanelInDB.Meta.CanEdit, - "folderName": libraryPanelInDB.Meta.FolderName, - "folderUid": libraryPanelInDB.Meta.FolderUID, - "connectedDashboards": libraryPanelInDB.Meta.ConnectedDashboards, - "created": libraryPanelInDB.Meta.Created, - "updated": libraryPanelInDB.Meta.Updated, + "folderName": elementInDB.Meta.FolderName, + "folderUid": elementInDB.Meta.FolderUID, + "connectedDashboards": elementInDB.Meta.Connections, + "created": elementInDB.Meta.Created, + "updated": elementInDB.Meta.Updated, "createdBy": map[string]interface{}{ - "id": libraryPanelInDB.Meta.CreatedBy.ID, - "name": libraryPanelInDB.Meta.CreatedBy.Name, - "avatarUrl": libraryPanelInDB.Meta.CreatedBy.AvatarUrl, + "id": elementInDB.Meta.CreatedBy.ID, + "name": elementInDB.Meta.CreatedBy.Name, + "avatarUrl": elementInDB.Meta.CreatedBy.AvatarURL, }, "updatedBy": map[string]interface{}{ - "id": libraryPanelInDB.Meta.UpdatedBy.ID, - "name": libraryPanelInDB.Meta.UpdatedBy.Name, - "avatarUrl": libraryPanelInDB.Meta.UpdatedBy.AvatarUrl, + "id": elementInDB.Meta.UpdatedBy.ID, + "name": elementInDB.Meta.UpdatedBy.Name, + "avatarUrl": elementInDB.Meta.UpdatedBy.AvatarURL, }, }, }) @@ -195,88 +196,5 @@ func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqCo libraryPanels = append(libraryPanels, uid) } - return lps.connectLibraryPanelsForDashboard(c, libraryPanels, dash.Id) -} - -// DisconnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and disconnects any library panels from the dashboard. -func (lps *LibraryPanelService) DisconnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error { - if !lps.IsEnabled() { - return nil - } - - panels := dash.Data.Get("panels").MustArray() - panelCount := int64(0) - for _, panel := range panels { - panelAsJSON := simplejson.NewFromAny(panel) - libraryPanel := panelAsJSON.Get("libraryPanel") - if libraryPanel.Interface() == nil { - continue - } - - // we have a library panel - uid := libraryPanel.Get("uid").MustString() - if len(uid) == 0 { - return errLibraryPanelHeaderUIDMissing - } - panelCount++ - } - - return lps.disconnectLibraryPanelsForDashboard(c, dash.Id, panelCount) -} - -func (lps *LibraryPanelService) DeleteLibraryPanelsInFolder(c *models.ReqContext, folderUID string) error { - if !lps.IsEnabled() { - return nil - } - return lps.deleteLibraryPanelsInFolder(c, folderUID) -} - -// AddMigration defines database migrations. -// If Panel Library is not enabled does nothing. -func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) { - if !lps.IsEnabled() { - return - } - - libraryPanelV1 := migrator.Table{ - Name: "library_panel", - Columns: []*migrator.Column{ - {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "org_id", Type: migrator.DB_BigInt, Nullable: false}, - {Name: "folder_id", Type: migrator.DB_BigInt, Nullable: false}, - {Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, - {Name: "name", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, - {Name: "type", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, - {Name: "description", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, - {Name: "model", Type: migrator.DB_Text, Nullable: false}, - {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, - {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}, - }, - } - - 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])) + return lps.LibraryElementService.ConnectElementsToDashboard(c, libraryPanels, dash.Id) } diff --git a/pkg/services/librarypanels/librarypanels_connections_test.go b/pkg/services/librarypanels/librarypanels_connections_test.go deleted file mode 100644 index 4ec3a1d71c0..00000000000 --- a/pkg/services/librarypanels/librarypanels_connections_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package librarypanels - -import ( - "encoding/json" - "strconv" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestConnectLibraryPanel(t *testing.T) { - scenarioWithLibraryPanel(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"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to create a connection that already exists, it should succeed", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - }) -} - -func TestDisconnectLibraryPanel(t *testing.T) { - scenarioWithLibraryPanel(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"}) - resp := sc.service.disconnectHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to remove a connection that does not exist, it should fail", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.disconnectHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to remove a connection that does exist, it should succeed", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - resp = sc.service.disconnectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - }) -} - -func TestGetConnectedDashboards(t *testing.T) { - scenarioWithLibraryPanel(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"}) - resp := sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) - }) - - scenarioWithLibraryPanel(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) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp := sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - var dashResult libraryPanelDashboardsResult - err := json.Unmarshal(resp.Body(), &dashResult) - require.NoError(t, err) - require.Equal(t, 0, len(dashResult.Result)) - }) - - scenarioWithLibraryPanel(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) { - firstDash := createDashboard(t, sc.sqlStore, sc.user, "Dash 1", 0) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": strconv.FormatInt(firstDash.Id, 10)}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - secondDash := createDashboard(t, sc.sqlStore, sc.user, "Dash 2", 0) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": strconv.FormatInt(secondDash.Id, 10)}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp = sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - var dashResult libraryPanelDashboardsResult - err := json.Unmarshal(resp.Body(), &dashResult) - require.NoError(t, err) - require.Equal(t, 2, len(dashResult.Result)) - require.Equal(t, firstDash.Id, dashResult.Result[0]) - require.Equal(t, secondDash.Id, dashResult.Result[1]) - }) -} diff --git a/pkg/services/librarypanels/librarypanels_delete_test.go b/pkg/services/librarypanels/librarypanels_delete_test.go deleted file mode 100644 index 6e08cb7b48e..00000000000 --- a/pkg/services/librarypanels/librarypanels_delete_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package librarypanels - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/models" -) - -func TestDeleteLibraryPanel(t *testing.T) { - scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that does not exist, it should fail", - func(t *testing.T, sc scenarioContext) { - resp := sc.service.deleteHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that exists, it should succeed", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp := sc.service.deleteHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel in another org, it should fail", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - sc.reqContext.SignedInUser.OrgId = 2 - sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN - resp := sc.service.deleteHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that is connected, it should fail", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp = sc.service.deleteHandler(sc.reqContext) - require.Equal(t, 403, resp.Status()) - }) -} diff --git a/pkg/services/librarypanels/librarypanels_get_test.go b/pkg/services/librarypanels/librarypanels_get_test.go deleted file mode 100644 index 46be66d27a0..00000000000 --- a/pkg/services/librarypanels/librarypanels_get_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package librarypanels - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/models" -) - -func TestGetLibraryPanel(t *testing.T) { - scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that does not exist, it should fail", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"}) - resp := sc.service.getHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that exists, it should succeed and return correct result", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp := sc.service.getHandler(sc.reqContext) - var result = validateAndUnMarshalResponse(t, resp) - var expected = libraryPanelResult{ - Result: libraryPanel{ - ID: 1, - OrgID: 1, - FolderID: 1, - UID: result.Result.UID, - Name: "Text - Library Panel", - Type: "text", - Description: "A description", - Model: map[string]interface{}{ - "datasource": "${DS_GDEV-TESTDATA}", - "description": "A description", - "id": float64(1), - "title": "Text - Library Panel", - "type": "text", - }, - Version: 1, - Meta: LibraryPanelDTOMeta{ - CanEdit: true, - FolderName: "ScenarioFolder", - FolderUID: sc.folder.Uid, - ConnectedDashboards: 0, - Created: result.Result.Meta.Created, - Updated: result.Result.Meta.Updated, - CreatedBy: LibraryPanelDTOMetaUser{ - ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, - }, - UpdatedBy: LibraryPanelDTOMetaUser{ - ID: 1, - Name: UserInDbName, - AvatarUrl: UserInDbAvatar, - }, - }, - }, - } - if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { - t.Fatalf("Result mismatch (-want +got):\n%s", diff) - } - }) - - scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that exists in an other org, it should fail", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - sc.reqContext.SignedInUser.OrgId = 2 - sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN - resp := sc.service.getHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to get a library panel with 2 connected dashboards, it should succeed and return correct connected dashboards", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "2"}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp = sc.service.getHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - var result = validateAndUnMarshalResponse(t, resp) - require.Equal(t, int64(2), result.Result.Meta.ConnectedDashboards) - }) -} diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 84d23465984..8863254816c 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -9,15 +9,16 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" "gopkg.in/macaron.v1" - "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -28,10 +29,6 @@ const UserInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad" func TestLoadLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel, it should copy JSON properties from library panel", func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - dashJSON := map[string]interface{}{ "panels": []interface{}{ map[string]interface{}{ @@ -59,13 +56,19 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) { }, } dash := models.Dashboard{ - Id: 1, - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing LoadLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + err := sc.elementService.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id) + require.NoError(t, err) - err := sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, &dash) + err = sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, dashInDB) require.NoError(t, err) expectedJSON := map[string]interface{}{ + "title": "Testing LoadLibraryPanelsForDashboard", + "uid": dashInDB.Uid, + "version": dashInDB.Version, "panels": []interface{}{ map[string]interface{}{ "id": int64(1), @@ -93,7 +96,6 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) { "description": sc.initialResult.Result.Description, "version": sc.initialResult.Result.Version, "meta": map[string]interface{}{ - "canEdit": false, "folderName": "ScenarioFolder", "folderUid": sc.folder.Uid, "connectedDashboards": int64(1), @@ -124,10 +126,6 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel without uid, it should fail", func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - dashJSON := map[string]interface{}{ "panels": []interface{}{ map[string]interface{}{ @@ -154,11 +152,14 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) { }, } dash := models.Dashboard{ - Id: 1, - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing LoadLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + err := sc.elementService.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id) + require.NoError(t, err) - err := sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, &dash) + err = sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, dashInDB) require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) }) @@ -191,13 +192,17 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) { }, } dash := models.Dashboard{ - Id: 1, - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing LoadLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) - err := sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, &dash) + err := sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, dashInDB) require.NoError(t, err) expectedJSON := map[string]interface{}{ + "title": "Testing LoadLibraryPanelsForDashboard", + "uid": dashInDB.Uid, + "version": dashInDB.Version, "panels": []interface{}{ map[string]interface{}{ "id": int64(1), @@ -264,13 +269,17 @@ func TestCleanLibraryPanelsForDashboard(t *testing.T) { }, } dash := models.Dashboard{ - Id: 1, - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing CleanLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) - err := sc.service.CleanLibraryPanelsForDashboard(&dash) + err := sc.service.CleanLibraryPanelsForDashboard(dashInDB) require.NoError(t, err) expectedJSON := map[string]interface{}{ + "title": "Testing CleanLibraryPanelsForDashboard", + "uid": dashInDB.Uid, + "version": dashInDB.Version, "panels": []interface{}{ map[string]interface{}{ "id": int64(1), @@ -333,11 +342,12 @@ func TestCleanLibraryPanelsForDashboard(t *testing.T) { }, } dash := models.Dashboard{ - Id: 1, - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing CleanLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) - err := sc.service.CleanLibraryPanelsForDashboard(&dash) + err := sc.service.CleanLibraryPanelsForDashboard(dashInDB) require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) }) @@ -372,11 +382,12 @@ func TestCleanLibraryPanelsForDashboard(t *testing.T) { }, } dash := models.Dashboard{ - Id: 1, - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing CleanLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) - err := sc.service.CleanLibraryPanelsForDashboard(&dash) + err := sc.service.CleanLibraryPanelsForDashboard(dashInDB) require.EqualError(t, err, errLibraryPanelHeaderNameMissing.Error()) }) } @@ -414,22 +425,18 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, } dash := models.Dashboard{ - Id: int64(1), - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing ConnectLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) - err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash) + err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, dashInDB) require.NoError(t, err) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp := sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - var dashResult libraryPanelDashboardsResult - err = json.Unmarshal(resp.Body(), &dashResult) + elements, err := sc.elementService.GetElementsForDashboard(sc.reqContext, dashInDB.Id) require.NoError(t, err) - require.Len(t, dashResult.Result, 1) - require.Equal(t, int64(1), dashResult.Result[0]) + require.Len(t, elements, 1) + require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) }) scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail", @@ -463,23 +470,32 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, } dash := models.Dashboard{ - Id: int64(1), - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing ConnectLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) - err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash) + err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, dashInDB) require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) }) scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with unused/removed library panels, it should disconnect unused/removed library panels", func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Unused Libray Panel") - resp := sc.service.createHandler(sc.reqContext, command) - var unused = validateAndUnMarshalResponse(t, resp) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": unused.Result.UID, ":dashboardId": "1"}) - resp = sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - + unused, err := sc.elementService.CreateElement(sc.reqContext, libraryelements.CreateLibraryElementCommand{ + FolderID: sc.folder.Id, + Name: "Unused Libray Panel", + Model: []byte(` + { + "datasource": "${DS_GDEV-TESTDATA}", + "id": 4, + "title": "Unused Libray Panel", + "type": "text", + "description": "Unused description" + } + `), + Kind: int64(libraryelements.Panel), + }) + require.NoError(t, err) dashJSON := map[string]interface{}{ "panels": []interface{}{ map[string]interface{}{ @@ -492,7 +508,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, }, map[string]interface{}{ - "id": int64(2), + "id": int64(4), "gridPos": map[string]interface{}{ "h": 6, "w": 6, @@ -501,258 +517,124 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, "datasource": "${DS_GDEV-TESTDATA}", "libraryPanel": map[string]interface{}{ - "uid": sc.initialResult.Result.UID, - "name": sc.initialResult.Result.Name, + "uid": unused.UID, + "name": unused.Name, }, - "title": "Text - Library Panel", - "type": "text", + "title": "Unused Libray Panel", + "description": "Unused description", }, }, } + dash := models.Dashboard{ - Id: int64(1), - Data: simplejson.NewFromAny(dashJSON), + Title: "Testing ConnectLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), } - - err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + err = sc.elementService.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id) require.NoError(t, err) - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp = sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - var existingResult libraryPanelDashboardsResult - err = json.Unmarshal(resp.Body(), &existingResult) - require.NoError(t, err) - require.Len(t, existingResult.Result, 1) - require.Equal(t, int64(1), existingResult.Result[0]) - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": unused.Result.UID}) - resp = sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - var unusedResult libraryPanelDashboardsResult - err = json.Unmarshal(resp.Body(), &unusedResult) - require.NoError(t, err) - require.Len(t, unusedResult.Result, 0) - }) -} - -func TestDisconnectLibraryPanelsForDashboard(t *testing.T) { - scenarioWithLibraryPanel(t, "When an admin tries to delete a dashboard with a library panel, it should disconnect the two", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - dashJSON := map[string]interface{}{ - "panels": []interface{}{ - map[string]interface{}{ - "id": int64(1), - "gridPos": map[string]interface{}{ - "h": 6, - "w": 6, - "x": 0, - "y": 0, - }, - }, - map[string]interface{}{ - "id": int64(2), - "gridPos": map[string]interface{}{ - "h": 6, - "w": 6, - "x": 6, - "y": 0, - }, - "datasource": "${DS_GDEV-TESTDATA}", - "libraryPanel": map[string]interface{}{ - "uid": sc.initialResult.Result.UID, - "name": sc.initialResult.Result.Name, - }, - "title": "Text - Library Panel", - "type": "text", + panelJSON := []interface{}{ + map[string]interface{}{ + "id": int64(1), + "gridPos": map[string]interface{}{ + "h": 6, + "w": 6, + "x": 0, + "y": 0, }, }, - } - dash := models.Dashboard{ - Id: int64(1), - Data: simplejson.NewFromAny(dashJSON), - } - - err := sc.service.DisconnectLibraryPanelsForDashboard(sc.reqContext, &dash) - require.NoError(t, err) - - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) - resp = sc.service.getConnectedDashboardsHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - var dashResult libraryPanelDashboardsResult - err = json.Unmarshal(resp.Body(), &dashResult) - require.NoError(t, err) - require.Empty(t, dashResult.Result) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to delete a dashboard with a library panel without uid, it should fail", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - dashJSON := map[string]interface{}{ - "panels": []interface{}{ - map[string]interface{}{ - "id": int64(1), - "gridPos": map[string]interface{}{ - "h": 6, - "w": 6, - "x": 0, - "y": 0, - }, + map[string]interface{}{ + "id": int64(2), + "gridPos": map[string]interface{}{ + "h": 6, + "w": 6, + "x": 6, + "y": 0, }, - map[string]interface{}{ - "id": int64(2), - "gridPos": map[string]interface{}{ - "h": 6, - "w": 6, - "x": 6, - "y": 0, - }, - "datasource": "${DS_GDEV-TESTDATA}", - "libraryPanel": map[string]interface{}{ - "name": sc.initialResult.Result.Name, - }, - "title": "Text - Library Panel", - "type": "text", + "datasource": "${DS_GDEV-TESTDATA}", + "libraryPanel": map[string]interface{}{ + "uid": sc.initialResult.Result.UID, + "name": sc.initialResult.Result.Name, }, + "title": "Text - Library Panel", + "type": "text", }, } - dash := models.Dashboard{ - Id: int64(1), - Data: simplejson.NewFromAny(dashJSON), - } - - err := sc.service.DisconnectLibraryPanelsForDashboard(sc.reqContext, &dash) - require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) - }) -} - -func TestDeleteLibraryPanelsInFolder(t *testing.T) { - scenarioWithLibraryPanel(t, "When an admin tries to delete a folder that contains connected library panels, it should fail", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"}) - resp := sc.service.connectHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - - err := sc.service.DeleteLibraryPanelsInFolder(sc.reqContext, sc.folder.Uid) - require.EqualError(t, err, ErrFolderHasConnectedLibraryPanels.Error()) - }) - - scenarioWithLibraryPanel(t, "When an admin tries to delete a folder that contains disconnected library panels, it should delete all disconnected library panels too", - func(t *testing.T, sc scenarioContext) { - resp := sc.service.getAllHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - var result libraryPanelsSearch - err := json.Unmarshal(resp.Body(), &result) + dashInDB.Data.Set("panels", panelJSON) + err = sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, dashInDB) require.NoError(t, err) - require.NotNil(t, result.Result) - require.Equal(t, 1, len(result.Result.LibraryPanels)) - err = sc.service.DeleteLibraryPanelsInFolder(sc.reqContext, sc.folder.Uid) + elements, err := sc.elementService.GetElementsForDashboard(sc.reqContext, dashInDB.Id) require.NoError(t, err) - resp = sc.service.getAllHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - err = json.Unmarshal(resp.Body(), &result) - require.NoError(t, err) - require.NotNil(t, result.Result) - require.Equal(t, 0, len(result.Result.LibraryPanels)) + require.Len(t, elements, 1) + require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) }) } 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 + OrgID int64 + FolderID int64 + UID string + Name string Type string Description string - Model map[string]interface{} `json:"model"` - Version int64 `json:"version"` - Meta LibraryPanelDTOMeta `json:"meta"` + Model map[string]interface{} + Version int64 + Meta libraryelements.LibraryElementDTOMeta } type libraryPanelResult struct { Result libraryPanel `json:"result"` } -type libraryPanelsSearch struct { - Result libraryPanelsSearchResult `json:"result"` -} - -type libraryPanelsSearchResult struct { - TotalCount int64 `json:"totalCount"` - LibraryPanels []libraryPanel `json:"libraryPanels"` - Page int `json:"page"` - PerPage int `json:"perPage"` -} - -type libraryPanelDashboardsResult struct { - Result []int64 `json:"result"` -} - -func overrideLibraryPanelServiceInRegistry(cfg *setting.Cfg) LibraryPanelService { - lps := LibraryPanelService{ +func overrideLibraryServicesInRegistry(cfg *setting.Cfg) (*LibraryPanelService, *libraryelements.LibraryElementService) { + les := libraryelements.LibraryElementService{ SQLStore: nil, Cfg: cfg, } - overrideServiceFunc := func(d registry.Descriptor) (*registry.Descriptor, bool) { + elementsOverride := func(d registry.Descriptor) (*registry.Descriptor, bool) { descriptor := registry.Descriptor{ - Name: "LibraryPanelService", - Instance: &lps, - InitPriority: 0, + Name: "LibraryElementService", + Instance: &les, } return &descriptor, true } - registry.RegisterOverride(overrideServiceFunc) + registry.RegisterOverride(elementsOverride) - return lps -} - -func getCreateCommand(folderID int64, name string) createLibraryPanelCommand { - command := getCreateCommandWithModel(folderID, name, []byte(` - { - "datasource": "${DS_GDEV-TESTDATA}", - "id": 1, - "title": "Text - Library Panel", - "type": "text", - "description": "A description" - } - `)) - - return command -} - -func getCreateCommandWithModel(folderID int64, name string, model []byte) createLibraryPanelCommand { - command := createLibraryPanelCommand{ - FolderID: folderID, - Name: name, - Model: model, + lps := LibraryPanelService{ + SQLStore: nil, + Cfg: cfg, + LibraryElementService: &les, } - return command + panelsOverride := func(d registry.Descriptor) (*registry.Descriptor, bool) { + descriptor := registry.Descriptor{ + Name: "LibraryPanelService", + Instance: &lps, + } + + return &descriptor, true + } + + registry.RegisterOverride(panelsOverride) + + return &lps, &les } type scenarioContext struct { - ctx *macaron.Context - service *LibraryPanelService - reqContext *models.ReqContext - user models.SignedInUser - folder *models.Folder - initialResult libraryPanelResult - sqlStore *sqlstore.SQLStore + ctx *macaron.Context + service *LibraryPanelService + elementService *libraryelements.LibraryElementService + reqContext *models.ReqContext + user models.SignedInUser + folder *models.Folder + initialResult libraryPanelResult + sqlStore *sqlstore.SQLStore } type folderACLItem struct { @@ -760,9 +642,7 @@ type folderACLItem struct { permission models.PermissionType } -func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, title string, - folderID int64) *models.Dashboard { - dash := models.NewDashboard(title) +func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard { dash.FolderId = folderID dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, @@ -824,25 +704,44 @@ func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, require.NoError(t, err) } -func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryPanelResult { - t.Helper() - - require.Equal(t, 200, resp.Status()) - - var result = libraryPanelResult{} - err := json.Unmarshal(resp.Body(), &result) - require.NoError(t, err) - - return result -} - func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { t.Helper() testScenario(t, desc, func(t *testing.T, sc scenarioContext) { - command := getCreateCommand(sc.folder.Id, "Text - Library Panel") - resp := sc.service.createHandler(sc.reqContext, command) - sc.initialResult = validateAndUnMarshalResponse(t, resp) + command := libraryelements.CreateLibraryElementCommand{ + FolderID: sc.folder.Id, + Name: "Text - Library Panel", + Model: []byte(` + { + "datasource": "${DS_GDEV-TESTDATA}", + "id": 1, + "title": "Text - Library Panel", + "type": "text", + "description": "A description" + } + `), + Kind: int64(libraryelements.Panel), + } + resp, err := sc.elementService.CreateElement(sc.reqContext, command) + require.NoError(t, err) + var model map[string]interface{} + err = json.Unmarshal(resp.Model, &model) + require.NoError(t, err) + + sc.initialResult = libraryPanelResult{ + Result: libraryPanel{ + ID: resp.ID, + OrgID: resp.OrgID, + FolderID: resp.FolderID, + UID: resp.UID, + Name: resp.Name, + Type: resp.Type, + Description: resp.Description, + Model: model, + Version: resp.Version, + Meta: resp.Meta, + }, + } fn(t, sc) }) @@ -867,10 +766,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo cfg.FeatureToggles = map[string]bool{"panelLibrary": true} // Because the LibraryPanelService is behind a feature toggle, we need to override the service in the registry // with a Cfg that contains the feature toggle so migrations are run properly - service := overrideLibraryPanelServiceInRegistry(cfg) + service, elementService := overrideLibraryServicesInRegistry(cfg) // We need to assign SQLStore after the override and migrations are done sqlStore := sqlstore.InitTestDB(t) + elementService.SQLStore = sqlStore service.SQLStore = sqlStore user := models.SignedInUser{ @@ -895,10 +795,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo require.NoError(t, err) sc := scenarioContext{ - user: user, - ctx: &ctx, - service: &service, - sqlStore: sqlStore, + user: user, + ctx: &ctx, + service: service, + elementService: elementService, + sqlStore: sqlStore, reqContext: &models.ReqContext{ Context: &ctx, SignedInUser: &user, diff --git a/pkg/services/librarypanels/models.go b/pkg/services/librarypanels/models.go index 09c918be4b1..2b3aa8571e0 100644 --- a/pkg/services/librarypanels/models.go +++ b/pkg/services/librarypanels/models.go @@ -1,154 +1,12 @@ package librarypanels import ( - "encoding/json" "errors" - "time" ) -// LibraryPanel is the model for library panel definitions. -type LibraryPanel struct { - ID int64 `xorm:"pk autoincr 'id'"` - OrgID int64 `xorm:"org_id"` - FolderID int64 `xorm:"folder_id"` - UID string `xorm:"uid"` - Name string - Type string - Description string - Model json.RawMessage - Version int64 - - Created time.Time - Updated time.Time - - CreatedBy int64 - UpdatedBy int64 -} - -// LibraryPanelWithMeta is the model used to retrieve library panels with additional meta information. -type LibraryPanelWithMeta struct { - ID int64 `xorm:"pk autoincr 'id'"` - OrgID int64 `xorm:"org_id"` - FolderID int64 `xorm:"folder_id"` - UID string `xorm:"uid"` - Name string - Type string - Description string - Model json.RawMessage - Version int64 - - Created time.Time - Updated time.Time - - CanEdit bool - FolderName string - FolderUID string `xorm:"folder_uid"` - ConnectedDashboards int64 - CreatedBy int64 - UpdatedBy int64 - CreatedByName string - CreatedByEmail string - UpdatedByName string - UpdatedByEmail string -} - -// LibraryPanelDTO is the frontend DTO for library panels. -type LibraryPanelDTO struct { - ID int64 `json:"id"` - OrgID int64 `json:"orgId"` - FolderID int64 `json:"folderId"` - UID string `json:"uid"` - Name string `json:"name"` - Type string `json:"type"` - Description string `json:"description"` - Model json.RawMessage `json:"model"` - Version int64 `json:"version"` - Meta LibraryPanelDTOMeta `json:"meta"` -} - -// LibraryPanelSearchResult is the search result for library panels. -type LibraryPanelSearchResult struct { - TotalCount int64 `json:"totalCount"` - LibraryPanels []LibraryPanelDTO `json:"libraryPanels"` - Page int `json:"page"` - PerPage int `json:"perPage"` -} - -// LibraryPanelDTOMeta is the meta information for LibraryPanelDTO. -type LibraryPanelDTOMeta struct { - CanEdit bool `json:"canEdit"` - FolderName string `json:"folderName"` - FolderUID string `json:"folderUid"` - ConnectedDashboards int64 `json:"connectedDashboards"` - - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - - CreatedBy LibraryPanelDTOMetaUser `json:"createdBy"` - UpdatedBy LibraryPanelDTOMetaUser `json:"updatedBy"` -} - -// LibraryPanelDTOMetaUser is the meta information for user that creates/changes the library panel. -type LibraryPanelDTOMetaUser struct { - ID int64 `json:"id"` - Name string `json:"name"` - AvatarUrl string `json:"avatarUrl"` -} - -// 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") // errLibraryPanelHeaderUIDMissing is an error for when a library panel header is missing the uid property. errLibraryPanelHeaderUIDMissing = errors.New("library panel header is missing required property uid") // errLibraryPanelHeaderNameMissing is an error for when a library panel header is missing the name property. 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") - // errLibraryPanelHasConnectedDashboards is an error for when an user deletes a library panel that is connected to library panels. - errLibraryPanelHasConnectedDashboards = errors.New("the library panel is linked to dashboards") ) - -// Commands - -// createLibraryPanelCommand is the command for adding a LibraryPanel -type createLibraryPanelCommand struct { - FolderID int64 `json:"folderId"` - Name string `json:"name"` - Model json.RawMessage `json:"model"` -} - -// patchLibraryPanelCommand is the command for patching a LibraryPanel -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"` -} - -// searchLibraryPanelsQuery is the query used for searching for LibraryPanels -type searchLibraryPanelsQuery struct { - perPage int - page int - searchString string - sortDirection string - panelFilter string - excludeUID string - folderFilter string -} diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index d1000e3dffe..f6a80e3d15f 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -12,7 +12,7 @@ import store from 'app/core/store'; import { addPanel } from 'app/features/dashboard/state/reducers'; import { DashboardModel, PanelModel } from '../../state'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; -import { LibraryPanelDTO } from '../../../library-panels/types'; +import { LibraryElementDTO } from '../../../library-panels/types'; import { toPanelModelLibraryPanel } from '../../../library-panels/utils'; import { LibraryPanelsSearch, @@ -108,7 +108,7 @@ export const AddPanelWidgetUnconnected: React.FC = ({ panel, dashboard }) dashboard.removePanel(panel); }; - const onAddLibraryPanel = (panelInfo: LibraryPanelDTO) => { + const onAddLibraryPanel = (panelInfo: LibraryElementDTO) => { const { gridPos } = panel; const newPanel: PanelModel = { diff --git a/public/app/features/folders/FolderLibraryPanelsPage.tsx b/public/app/features/folders/FolderLibraryPanelsPage.tsx index 0e41c263348..db1e65079a4 100644 --- a/public/app/features/folders/FolderLibraryPanelsPage.tsx +++ b/public/app/features/folders/FolderLibraryPanelsPage.tsx @@ -6,7 +6,7 @@ import { GrafanaRouteComponentProps } from '../../core/navigation/types'; import { StoreState } from '../../types'; import { getNavModel } from '../../core/selectors/navModel'; import { getLoadingNav } from './state/navModel'; -import { LibraryPanelDTO } from '../library-panels/types'; +import { LibraryElementDTO } from '../library-panels/types'; import Page from '../../core/components/Page/Page'; import { LibraryPanelsSearch } from '../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; import { OpenLibraryPanelModal } from '../library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal'; @@ -33,7 +33,7 @@ export type Props = OwnProps & ConnectedProps; export function FolderLibraryPanelsPage({ navModel, getFolderByUid, folderUid, folder }: Props): JSX.Element { const { loading } = useAsync(async () => await getFolderByUid(folderUid), [getFolderByUid, folderUid]); - const [selected, setSelected] = useState(undefined); + const [selected, setSelected] = useState(undefined); return ( diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index 99150f64259..93b38e68429 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -22,6 +22,16 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { ], }; + if (getConfig().featureToggles.panelLibrary) { + model.children.push({ + active: false, + icon: 'library-panel', + id: `folder-library-panels-${folder.uid}`, + text: 'Panels', + url: `${folder.url}/library-panels`, + }); + } + if (folder.canAdmin) { model.children.push({ active: false, @@ -42,16 +52,6 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { }); } - if (getConfig().featureToggles.panelLibrary) { - model.children.push({ - active: false, - icon: 'library-panel', - id: `folder-library-panels-${folder.uid}`, - text: 'Panels', - url: `${folder.url}/library-panels`, - }); - } - return model; } diff --git a/public/app/features/library-panels/LibraryPanelsPage.tsx b/public/app/features/library-panels/LibraryPanelsPage.tsx index 868f272ef7c..e13d5937643 100644 --- a/public/app/features/library-panels/LibraryPanelsPage.tsx +++ b/public/app/features/library-panels/LibraryPanelsPage.tsx @@ -6,7 +6,7 @@ import { StoreState } from '../../types'; import { getNavModel } from '../../core/selectors/navModel'; import Page from '../../core/components/Page/Page'; import { LibraryPanelsSearch } from './components/LibraryPanelsSearch/LibraryPanelsSearch'; -import { LibraryPanelDTO } from './types'; +import { LibraryElementDTO } from './types'; import { OpenLibraryPanelModal } from './components/OpenLibraryPanelModal/OpenLibraryPanelModal'; const mapStateToProps = (state: StoreState) => ({ @@ -20,7 +20,7 @@ interface OwnProps extends GrafanaRouteComponentProps {} type Props = OwnProps & ConnectedProps; export const LibraryPanelsPage: FC = ({ navModel }) => { - const [selected, setSelected] = useState(undefined); + const [selected, setSelected] = useState(undefined); return ( diff --git a/public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx b/public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx index 87f8db6d10c..0f2da652370 100644 --- a/public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx @@ -2,14 +2,14 @@ import React, { FC, useEffect, useMemo, useReducer } from 'react'; import { Button, Modal, useStyles } from '@grafana/ui'; import { LoadingState } from '@grafana/data'; -import { LibraryPanelDTO } from '../../types'; +import { LibraryElementDTO } from '../../types'; import { asyncDispatcher } from '../LibraryPanelsView/actions'; import { deleteLibraryPanelModalReducer, initialDeleteLibraryPanelModalState } from './reducer'; import { getConnectedDashboards } from './actions'; import { getModalStyles } from '../../styles'; interface Props { - libraryPanel: LibraryPanelDTO; + libraryPanel: LibraryElementDTO; onConfirm: () => void; onDismiss: () => void; } diff --git a/public/app/features/library-panels/components/DeleteLibraryPanelModal/actions.ts b/public/app/features/library-panels/components/DeleteLibraryPanelModal/actions.ts index 3cf7004be89..0d7784495c8 100644 --- a/public/app/features/library-panels/components/DeleteLibraryPanelModal/actions.ts +++ b/public/app/features/library-panels/components/DeleteLibraryPanelModal/actions.ts @@ -1,8 +1,8 @@ -import { DispatchResult, LibraryPanelDTO } from '../../types'; +import { DispatchResult, LibraryElementDTO } from '../../types'; import { getConnectedDashboards as apiGetConnectedDashboards } from '../../state/api'; import { searchCompleted } from './reducer'; -export function getConnectedDashboards(libraryPanel: LibraryPanelDTO): DispatchResult { +export function getConnectedDashboards(libraryPanel: LibraryElementDTO): DispatchResult { return async function (dispatch) { const dashboards = await apiGetConnectedDashboards(libraryPanel.uid); dispatch(searchCompleted({ dashboards })); diff --git a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx index 9dacf6e1d0a..17daf6e987f 100644 --- a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx +++ b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx @@ -4,14 +4,14 @@ import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Icon, Link, useStyles2 } from '@grafana/ui'; -import { LibraryPanelDTO } from '../../types'; +import { LibraryElementDTO } from '../../types'; import { PanelTypeCard } from 'app/features/dashboard/components/VizTypePicker/PanelTypeCard'; import { DeleteLibraryPanelModal } from '../DeleteLibraryPanelModal/DeleteLibraryPanelModal'; export interface LibraryPanelCardProps { - libraryPanel: LibraryPanelDTO; - onClick: (panel: LibraryPanelDTO) => void; - onDelete?: (panel: LibraryPanelDTO) => void; + libraryPanel: LibraryElementDTO; + onClick: (panel: LibraryElementDTO) => void; + onDelete?: (panel: LibraryElementDTO) => void; showSecondaryActions?: boolean; } @@ -54,7 +54,7 @@ export const LibraryPanelCard: React.FC - {libraryPanel.meta.folderName} + {libraryPanel.meta.folderName} ); } return ( - - + + - {libraryPanel.meta.folderName} - - + {libraryPanel.meta.folderName} + + ); } diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx index c29c0d79126..12bcc5a7f5c 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx @@ -6,7 +6,7 @@ import { PanelPluginMeta, PluginType } from '@grafana/data'; import { LibraryPanelsSearch, LibraryPanelsSearchProps } from './LibraryPanelsSearch'; import * as api from '../../state/api'; -import { LibraryPanelSearchResult } from '../../types'; +import { LibraryElementKind, LibraryElementsSearchResult } from '../../types'; import { backendSrv } from '../../../../core/services/backend_srv'; import * as viztypepicker from '../../../dashboard/components/VizTypePicker/VizTypePicker'; @@ -38,7 +38,7 @@ jest.mock('debounce-promise', () => { async function getTestContext( propOverrides: Partial = {}, - searchResult: LibraryPanelSearchResult = { libraryPanels: [], perPage: 40, page: 1, totalCount: 0 } + searchResult: LibraryElementsSearchResult = { elements: [], perPage: 40, page: 1, totalCount: 0 } ) { jest.clearAllMocks(); const pluginInfo: any = { logos: { small: '', large: '' } }; @@ -102,7 +102,7 @@ describe('LibraryPanelsSearch', () => { searchString: 'a', folderFilter: [], page: 0, - panelFilter: [], + typeFilter: [], perPage: 40, }); }); @@ -130,7 +130,7 @@ describe('LibraryPanelsSearch', () => { sortDirection: 'alpha-desc', folderFilter: [], page: 0, - panelFilter: [], + typeFilter: [], perPage: 40, }); }); @@ -158,7 +158,7 @@ describe('LibraryPanelsSearch', () => { searchString: '', folderFilter: [], page: 0, - panelFilter: ['graph', 'timeseries'], + typeFilter: ['graph', 'timeseries'], perPage: 40, }); }); @@ -188,7 +188,7 @@ describe('LibraryPanelsSearch', () => { searchString: '', folderFilter: ['0'], page: 0, - panelFilter: [], + typeFilter: [], perPage: 40, }); }); @@ -203,10 +203,11 @@ describe('LibraryPanelsSearch', () => { page: 1, totalCount: 1, perPage: 40, - libraryPanels: [ + elements: [ { id: 1, name: 'Library Panel Name', + kind: LibraryElementKind.Panel, uid: 'uid', description: 'Library Panel Description', folderId: 0, @@ -215,7 +216,6 @@ describe('LibraryPanelsSearch', () => { orgId: 1, version: 1, meta: { - canEdit: true, folderName: 'General', folderUid: '', connectedDashboards: 0, @@ -247,10 +247,11 @@ describe('LibraryPanelsSearch', () => { page: 1, totalCount: 1, perPage: 40, - libraryPanels: [ + elements: [ { id: 1, name: 'Library Panel Name', + kind: LibraryElementKind.Panel, uid: 'uid', description: 'Library Panel Description', folderId: 0, @@ -259,7 +260,6 @@ describe('LibraryPanelsSearch', () => { orgId: 1, version: 1, meta: { - canEdit: true, folderName: 'General', folderUid: '', connectedDashboards: 0, diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx index db4efcce1d5..77660dc0fb3 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx @@ -7,7 +7,7 @@ import { SortPicker } from '../../../../core/components/Select/SortPicker'; import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter'; import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../core/constants'; -import { LibraryPanelDTO } from '../../types'; +import { LibraryElementDTO } from '../../types'; import { FolderFilter } from '../../../../core/components/FolderFilter/FolderFilter'; import { FolderInfo } from '../../../../types'; import { @@ -25,7 +25,7 @@ export enum LibraryPanelsSearchVariant { } export interface LibraryPanelsSearchProps { - onClick: (panel: LibraryPanelDTO) => void; + onClick: (panel: LibraryElementDTO) => void; variant?: LibraryPanelsSearchVariant; showSort?: boolean; showPanelFilter?: boolean; diff --git a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx index a3eb3d05d2a..cecfec6d7be 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx @@ -5,13 +5,13 @@ import { Pagination, useStyles } from '@grafana/ui'; import { GrafanaTheme, LoadingState } from '@grafana/data'; import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard'; -import { LibraryPanelDTO } from '../../types'; +import { LibraryElementDTO } from '../../types'; import { changePage, initialLibraryPanelsViewState, libraryPanelsViewReducer } from './reducer'; import { asyncDispatcher, deleteLibraryPanel, searchForLibraryPanels } from './actions'; interface LibraryPanelViewProps { className?: string; - onClickCard: (panel: LibraryPanelDTO) => void; + onClickCard: (panel: LibraryElementDTO) => void; showSecondaryActions?: boolean; currentPanelId?: string; searchString: string; @@ -58,7 +58,7 @@ export const LibraryPanelsView: React.FC = ({ 300, [searchString, sortDirection, panelFilter, folderFilter, page, asyncDispatch] ); - const onDelete = ({ uid }: LibraryPanelDTO) => + const onDelete = ({ uid }: LibraryElementDTO) => asyncDispatch(deleteLibraryPanel(uid, { searchString, page, perPage })); const onPageChange = (page: number) => asyncDispatch(changePage({ page })); diff --git a/public/app/features/library-panels/components/LibraryPanelsView/actions.ts b/public/app/features/library-panels/components/LibraryPanelsView/actions.ts index d0774194381..8dbd962787c 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/actions.ts +++ b/public/app/features/library-panels/components/LibraryPanelsView/actions.ts @@ -27,11 +27,11 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult { page: args.page, excludeUid: args.currentPanelId, sortDirection: args.sortDirection, - panelFilter: args.panelFilter, + typeFilter: args.panelFilter, folderFilter: args.folderFilter, }) ).pipe( - mergeMap(({ perPage, libraryPanels, page, totalCount }) => + mergeMap(({ perPage, elements: libraryPanels, page, totalCount }) => of(searchCompleted({ libraryPanels, page, perPage, totalCount })) ), catchError((err) => { diff --git a/public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts b/public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts index 131d500fb87..9b9864d5bc3 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts +++ b/public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts @@ -9,7 +9,7 @@ import { LibraryPanelsViewState, searchCompleted, } from './reducer'; -import { LibraryPanelDTO } from '../../types'; +import { LibraryElementDTO, LibraryElementKind } from '../../types'; describe('libraryPanelsViewReducer', () => { describe('when initSearch is dispatched', () => { @@ -85,8 +85,8 @@ describe('libraryPanelsViewReducer', () => { }); }); -function getLibraryPanelMocks(count: number): LibraryPanelDTO[] { - const mocks: LibraryPanelDTO[] = []; +function getLibraryPanelMocks(count: number): LibraryElementDTO[] { + const mocks: LibraryElementDTO[] = []; for (let i = 0; i < count; i++) { mocks.push( @@ -109,7 +109,6 @@ function mockLibraryPanel({ name = 'Test Panel', model = { type: 'text', title: 'Test Panel' }, meta = { - canEdit: true, folderName: 'General', folderUid: '', connectedDashboards: 0, @@ -121,13 +120,14 @@ function mockLibraryPanel({ version = 1, description = 'a description', type = 'text', -}: Partial = {}): LibraryPanelDTO { +}: Partial = {}): LibraryElementDTO { return { uid, id, orgId, folderId, name, + kind: LibraryElementKind.Panel, model, version, meta, diff --git a/public/app/features/library-panels/components/LibraryPanelsView/reducer.ts b/public/app/features/library-panels/components/LibraryPanelsView/reducer.ts index c8ef978826e..4d4726f0183 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/reducer.ts +++ b/public/app/features/library-panels/components/LibraryPanelsView/reducer.ts @@ -1,12 +1,12 @@ import { createAction } from '@reduxjs/toolkit'; import { LoadingState } from '@grafana/data'; -import { LibraryPanelDTO } from '../../types'; +import { LibraryElementDTO } from '../../types'; import { AnyAction } from 'redux'; export interface LibraryPanelsViewState { loadingState: LoadingState; - libraryPanels: LibraryPanelDTO[]; + libraryPanels: LibraryElementDTO[]; totalCount: number; perPage: number; page: number; diff --git a/public/app/features/library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal.tsx b/public/app/features/library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal.tsx index 76da4eb275c..0df0b937ad8 100644 --- a/public/app/features/library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal.tsx @@ -4,14 +4,14 @@ import { AsyncSelect, Button, Modal, useStyles2 } from '@grafana/ui'; import { GrafanaTheme2, SelectableValue, urlUtil } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { LibraryPanelDTO } from '../../types'; +import { LibraryElementDTO } from '../../types'; import { DashboardSearchHit } from '../../../search/types'; import { getConnectedDashboards, getLibraryPanelConnectedDashboards } from '../../state/api'; import { debounce } from 'lodash'; export interface OpenLibraryPanelModalProps { onDismiss: () => void; - libraryPanel: LibraryPanelDTO; + libraryPanel: LibraryElementDTO; } export function OpenLibraryPanelModal({ libraryPanel, onDismiss }: OpenLibraryPanelModalProps): JSX.Element { @@ -47,8 +47,11 @@ export function OpenLibraryPanelModal({ libraryPanel, onDismiss }: OpenLibraryPa {connected > 0 ? ( <>

- This panel is being used in {connected} dashboards.Please choose which dashboard to view - the panel in: + This panel is being used in{' '} + + {connected} {connected > 1 ? 'dashboards' : 'dashboard'} + + .Please choose which dashboard to view the panel in:

= ({ panel, searchQuery }) => { const styles = useStyles2(getStyles); const [showingAddPanelModal, setShowingAddPanelModal] = useState(false); - const [changeToPanel, setChangeToPanel] = useState(undefined); + const [changeToPanel, setChangeToPanel] = useState(undefined); const [panelFilter, setPanelFilter] = useState([]); const onPanelFilterChange = useCallback( (plugins: PanelPluginMeta[]) => { @@ -63,7 +63,7 @@ export const PanelLibraryOptionsGroup: FC = ({ panel, searchQuery }) => { setShowingAddPanelModal(true); }; - const onChangeLibraryPanel = (panel: LibraryPanelDTO) => { + const onChangeLibraryPanel = (panel: LibraryElementDTO) => { setChangeToPanel(panel); }; diff --git a/public/app/features/library-panels/state/api.ts b/public/app/features/library-panels/state/api.ts index f4e60c6658b..81d83569c8d 100644 --- a/public/app/features/library-panels/state/api.ts +++ b/public/app/features/library-panels/state/api.ts @@ -1,4 +1,10 @@ -import { LibraryPanelDTO, LibraryPanelSearchResult, PanelModelWithLibraryPanel } from '../types'; +import { + LibraryElementConnectionDTO, + LibraryElementDTO, + LibraryElementKind, + LibraryElementsSearchResult, + PanelModelWithLibraryPanel, +} from '../types'; import { DashboardSearchHit } from '../../search/types'; import { getBackendSrv } from '../../../core/services/backend_srv'; @@ -8,7 +14,7 @@ export interface GetLibraryPanelsOptions { page?: number; excludeUid?: string; sortDirection?: string; - panelFilter?: string[]; + typeFilter?: string[]; folderFilter?: string[]; } @@ -18,35 +24,39 @@ export async function getLibraryPanels({ page = 1, excludeUid = '', sortDirection = '', - panelFilter = [], + typeFilter = [], folderFilter = [], -}: GetLibraryPanelsOptions = {}): Promise { +}: GetLibraryPanelsOptions = {}): Promise { const params = new URLSearchParams(); params.append('searchString', searchString); params.append('sortDirection', sortDirection); - params.append('panelFilter', panelFilter.join(',')); + params.append('typeFilter', typeFilter.join(',')); params.append('folderFilter', folderFilter.join(',')); params.append('excludeUid', excludeUid); params.append('perPage', perPage.toString(10)); params.append('page', page.toString(10)); + params.append('kind', LibraryElementKind.Panel.toString(10)); - const { result } = await getBackendSrv().get(`/api/library-panels?${params.toString()}`); + const { result } = await getBackendSrv().get<{ result: LibraryElementsSearchResult }>( + `/api/library-elements?${params.toString()}` + ); return result; } -export async function getLibraryPanel(uid: string): Promise { - const { result } = await getBackendSrv().get(`/api/library-panels/${uid}`); +export async function getLibraryPanel(uid: string): Promise { + const { result } = await getBackendSrv().get(`/api/library-elements/${uid}`); return result; } export async function addLibraryPanel( panelSaveModel: PanelModelWithLibraryPanel, folderId: number -): Promise { - const { result } = await getBackendSrv().post(`/api/library-panels`, { +): Promise { + const { result } = await getBackendSrv().post(`/api/library-elements`, { folderId, name: panelSaveModel.title, model: panelSaveModel, + kind: LibraryElementKind.Panel, }); return result; } @@ -54,31 +64,36 @@ export async function addLibraryPanel( export async function updateLibraryPanel( panelSaveModel: PanelModelWithLibraryPanel, folderId: number -): Promise { - const { result } = await getBackendSrv().patch(`/api/library-panels/${panelSaveModel.libraryPanel.uid}`, { +): Promise { + const { result } = await getBackendSrv().patch(`/api/library-elements/${panelSaveModel.libraryPanel.uid}`, { folderId, name: panelSaveModel.title, model: panelSaveModel, version: panelSaveModel.libraryPanel.version, + kind: LibraryElementKind.Panel, }); return result; } export function deleteLibraryPanel(uid: string): Promise<{ message: string }> { - return getBackendSrv().delete(`/api/library-panels/${uid}`); + return getBackendSrv().delete(`/api/library-elements/${uid}`); } -export async function getLibraryPanelConnectedDashboards(libraryPanelUid: string): Promise { - const { result } = await getBackendSrv().get(`/api/library-panels/${libraryPanelUid}/dashboards`); +export async function getLibraryPanelConnectedDashboards( + libraryPanelUid: string +): Promise { + const { result } = await getBackendSrv().get<{ result: LibraryElementConnectionDTO[] }>( + `/api/library-elements/${libraryPanelUid}/connections` + ); return result; } export async function getConnectedDashboards(uid: string): Promise { - const dashboardIds = await getLibraryPanelConnectedDashboards(uid); - if (dashboardIds.length === 0) { + const connections = await getLibraryPanelConnectedDashboards(uid); + if (connections.length === 0) { return []; } - const searchHits = await getBackendSrv().search({ dashboardIds }); + const searchHits = await getBackendSrv().search({ dashboardIds: connections.map((c) => c.connectionId) }); return searchHits; } diff --git a/public/app/features/library-panels/types.ts b/public/app/features/library-panels/types.ts index a4538c07f7e..b523cdca8d7 100644 --- a/public/app/features/library-panels/types.ts +++ b/public/app/features/library-panels/types.ts @@ -2,44 +2,62 @@ import { PanelModel } from '../dashboard/state'; import { Dispatch } from 'react'; import { AnyAction } from '@reduxjs/toolkit'; -export interface LibraryPanelSearchResult { +export enum LibraryElementKind { + Panel = 1, + Variable, +} + +export enum LibraryElementConnectionKind { + Dashboard = 1, +} + +export interface LibraryElementConnectionDTO { + id: number; + kind: LibraryElementConnectionKind; + elementId: number; + connectionId: number; + created: string; + createdBy: LibraryElementDTOMetaUser; +} + +export interface LibraryElementsSearchResult { totalCount: number; - libraryPanels: LibraryPanelDTO[]; + elements: LibraryElementDTO[]; perPage: number; page: number; } -export interface LibraryPanelDTO { +export interface LibraryElementDTO { id: number; orgId: number; folderId: number; uid: string; name: string; + kind: LibraryElementKind; type: string; description: string; model: any; version: number; - meta: LibraryPanelDTOMeta; + meta: LibraryElementDTOMeta; } -export interface LibraryPanelDTOMeta { - canEdit: boolean; +export interface LibraryElementDTOMeta { folderName: string; folderUid: string; connectedDashboards: number; created: string; updated: string; - createdBy: LibraryPanelDTOMetaUser; - updatedBy: LibraryPanelDTOMetaUser; + createdBy: LibraryElementDTOMetaUser; + updatedBy: LibraryElementDTOMetaUser; } -export interface LibraryPanelDTOMetaUser { +export interface LibraryElementDTOMetaUser { id: number; name: string; avatarUrl: string; } -export type PanelModelLibraryPanel = Pick; +export type PanelModelLibraryPanel = Pick; export interface PanelModelWithLibraryPanel extends PanelModel { libraryPanel: PanelModelLibraryPanel; diff --git a/public/app/features/library-panels/utils.ts b/public/app/features/library-panels/utils.ts index a76d2cc6d62..c412b749a24 100644 --- a/public/app/features/library-panels/utils.ts +++ b/public/app/features/library-panels/utils.ts @@ -1,4 +1,4 @@ -import { LibraryPanelDTO, PanelModelLibraryPanel } from './types'; +import { LibraryElementDTO, PanelModelLibraryPanel } from './types'; import { PanelModel } from '../dashboard/state'; import { addLibraryPanel, updateLibraryPanel } from './state/api'; import { createErrorNotification, createSuccessNotification } from '../../core/copy/appNotification'; @@ -12,12 +12,12 @@ export function createPanelLibrarySuccessNotification(message: string): AppNotif return createSuccessNotification(message); } -export function toPanelModelLibraryPanel(libraryPanelDto: LibraryPanelDTO): PanelModelLibraryPanel { +export function toPanelModelLibraryPanel(libraryPanelDto: LibraryElementDTO): PanelModelLibraryPanel { const { uid, name, meta, version } = libraryPanelDto; return { uid, name, meta, version }; } -export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise { +export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise { const panelSaveModel = toPanelSaveModel(panel); const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderId); updatePanelModelWithUpdate(panel, savedPanel); @@ -37,7 +37,7 @@ function toPanelSaveModel(panel: PanelModel): any { return panelSaveModel; } -function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryPanelDTO): void { +function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryElementDTO): void { panel.restoreModel({ ...updated.model, configRev: 0, // reset config rev, since changes have been saved @@ -46,7 +46,7 @@ function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryPanelDTO) panel.refresh(); } -function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise { +function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise { if (!panel.libraryPanel) { return Promise.reject(); }