mirror of https://github.com/grafana/grafana
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 <arve.knudsen@gmail.com> * Update pkg/services/libraryelements/libraryelements_permissions_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/libraryelements/database.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Chore: changes after PR comments * Update libraryelements.go * Chore: updates after PR comments Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>pull/33900/head
parent
9b12e79f3e
commit
f1b2c750e5
@ -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) |
||||
} |
||||
@ -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 |
||||
}) |
||||
} |
||||
@ -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])) |
||||
} |
||||
@ -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()) |
||||
}) |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -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()) |
||||
}) |
||||
} |
||||
@ -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() |
||||
}), |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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) |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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]) |
||||
}) |
||||
} |
||||
@ -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()) |
||||
}) |
||||
} |
||||
@ -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) |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue