mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
123
pkg/services/libraryelements/api.go
Normal file
123
pkg/services/libraryelements/api.go
Normal file
@@ -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)
|
||||
}
|
||||
688
pkg/services/libraryelements/database.go
Normal file
688
pkg/services/libraryelements/database.go
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
51
pkg/services/libraryelements/guard.go
Normal file
51
pkg/services/libraryelements/guard.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
)
|
||||
|
||||
func isGeneralFolder(folderID int64) bool {
|
||||
return folderID == 0
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) error {
|
||||
kind := LibraryElementKind(kindAsInt)
|
||||
switch kind {
|
||||
case Panel:
|
||||
return nil
|
||||
case Variable:
|
||||
return nil
|
||||
default:
|
||||
return errLibraryElementUnSupportedElementKind
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) requirePermissionsOnFolder(user *models.SignedInUser, folderID int64) error {
|
||||
if isGeneralFolder(folderID) && user.HasRole(models.ROLE_EDITOR) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isGeneralFolder(folderID) && user.HasRole(models.ROLE_VIEWER) {
|
||||
return models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
s := dashboards.NewFolderService(user.OrgId, user, l.SQLStore)
|
||||
folder, err := s.GetFolderByID(folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g := guardian.New(folder.Id, user.OrgId, user)
|
||||
|
||||
canEdit, err := g.CanEdit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canEdit {
|
||||
return models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
139
pkg/services/libraryelements/libraryelements.go
Normal file
139
pkg/services/libraryelements/libraryelements.go
Normal file
@@ -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]))
|
||||
}
|
||||
104
pkg/services/libraryelements/libraryelements_create_test.go
Normal file
104
pkg/services/libraryelements/libraryelements_create_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateLibraryElement(t *testing.T) {
|
||||
scenarioWithPanel(t, "When an admin tries to create a library panel that already exists, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 400, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists, it should succeed",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
var expected = libraryElementResult{
|
||||
Result: libraryElement{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: sc.initialResult.Result.UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "A description",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
Connections: 0,
|
||||
Created: sc.initialResult.Result.Meta.Created,
|
||||
Updated: sc.initialResult.Result.Meta.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: "signed_in_user",
|
||||
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: "signed_in_user",
|
||||
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, sc.initialResult, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
testScenario(t, "When an admin tries to create a library panel where name and panel title differ, it should update panel title",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreatePanelCommand(1, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
var expected = libraryElementResult{
|
||||
Result: libraryElement{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.UID,
|
||||
Name: "Library Panel Name",
|
||||
Kind: int64(Panel),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "A description",
|
||||
"id": float64(1),
|
||||
"title": "Library Panel Name",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
Connections: 0,
|
||||
Created: result.Result.Meta.Created,
|
||||
Updated: result.Result.Meta.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: "signed_in_user",
|
||||
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: "signed_in_user",
|
||||
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
76
pkg/services/libraryelements/libraryelements_delete_test.go
Normal file
76
pkg/services/libraryelements/libraryelements_delete_test.go
Normal file
@@ -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())
|
||||
})
|
||||
}
|
||||
1244
pkg/services/libraryelements/libraryelements_get_all_test.go
Normal file
1244
pkg/services/libraryelements/libraryelements_get_all_test.go
Normal file
File diff suppressed because it is too large
Load Diff
75
pkg/services/libraryelements/libraryelements_get_test.go
Normal file
75
pkg/services/libraryelements/libraryelements_get_test.go
Normal file
@@ -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())
|
||||
})
|
||||
}
|
||||
304
pkg/services/libraryelements/libraryelements_patch_test.go
Normal file
304
pkg/services/libraryelements/libraryelements_patch_test.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPatchLibraryElement(t *testing.T) {
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{Kind: int64(Panel)}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel that exists, it should succeed",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: newFolder.Id,
|
||||
Name: "Panel - New name",
|
||||
Model: []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "An updated description",
|
||||
"id": 1,
|
||||
"title": "Model - New name",
|
||||
"type": "graph"
|
||||
}
|
||||
`),
|
||||
Kind: int64(Panel),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
var expected = libraryElementResult{
|
||||
Result: libraryElement{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: newFolder.Id,
|
||||
UID: sc.initialResult.Result.UID,
|
||||
Name: "Panel - New name",
|
||||
Kind: int64(Panel),
|
||||
Type: "graph",
|
||||
Description: "An updated description",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "An updated description",
|
||||
"id": float64(1),
|
||||
"title": "Panel - New name",
|
||||
"type": "graph",
|
||||
},
|
||||
Version: 2,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
Connections: 0,
|
||||
Created: sc.initialResult.Result.Meta.Created,
|
||||
Updated: result.Result.Meta.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: userInDbName,
|
||||
AvatarURL: userInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: "signed_in_user",
|
||||
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: newFolder.Id,
|
||||
Kind: int64(Panel),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
sc.initialResult.Result.FolderID = newFolder.Id
|
||||
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
|
||||
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
sc.initialResult.Result.Version = 2
|
||||
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with name only, it should change name successfully, sync title and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: -1,
|
||||
Name: "New Name",
|
||||
Kind: int64(Panel),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
sc.initialResult.Result.Name = "New Name"
|
||||
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
|
||||
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
sc.initialResult.Result.Model["title"] = "New Name"
|
||||
sc.initialResult.Result.Version = 2
|
||||
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with model only, it should change model successfully, sync name, type and description fields and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: -1,
|
||||
Model: []byte(`{ "title": "New Model Title", "name": "New Model Name", "type":"graph", "description": "New description" }`),
|
||||
Kind: int64(Panel),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
sc.initialResult.Result.Type = "graph"
|
||||
sc.initialResult.Result.Description = "New description"
|
||||
sc.initialResult.Result.Model = map[string]interface{}{
|
||||
"title": "Text - Library Panel",
|
||||
"name": "New Model Name",
|
||||
"type": "graph",
|
||||
"description": "New description",
|
||||
}
|
||||
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
|
||||
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
sc.initialResult.Result.Version = 2
|
||||
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with model.description only, it should change model successfully, sync name, type and description fields and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: -1,
|
||||
Model: []byte(`{ "description": "New description" }`),
|
||||
Kind: int64(Panel),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
sc.initialResult.Result.Type = "text"
|
||||
sc.initialResult.Result.Description = "New description"
|
||||
sc.initialResult.Result.Model = map[string]interface{}{
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
"description": "New description",
|
||||
}
|
||||
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
|
||||
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
sc.initialResult.Result.Version = 2
|
||||
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with model.type only, it should change model successfully, sync name, type and description fields and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: -1,
|
||||
Model: []byte(`{ "type": "graph" }`),
|
||||
Kind: int64(Panel),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
sc.initialResult.Result.Type = "graph"
|
||||
sc.initialResult.Result.Description = "A description"
|
||||
sc.initialResult.Result.Model = map[string]interface{}{
|
||||
"title": "Text - Library Panel",
|
||||
"type": "graph",
|
||||
"description": "A description",
|
||||
}
|
||||
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
|
||||
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
sc.initialResult.Result.Version = 2
|
||||
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(Panel)}
|
||||
sc.reqContext.UserId = 2
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
sc.initialResult.Result.Meta.UpdatedBy.ID = int64(2)
|
||||
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
|
||||
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
sc.initialResult.Result.Version = 2
|
||||
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreatePanelCommand(sc.folder.Id, "Another Panel")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
cmd := patchLibraryElementCommand{
|
||||
Name: "Text - Library Panel",
|
||||
Version: 1,
|
||||
Kind: int64(Panel),
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 400, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
|
||||
command := getCreatePanelCommand(newFolder.Id, "Text - Library Panel")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: 1,
|
||||
Version: 1,
|
||||
Kind: int64(Panel),
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 400, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: sc.folder.Id,
|
||||
Version: 1,
|
||||
Kind: int64(Panel),
|
||||
}
|
||||
sc.reqContext.OrgId = 2
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with an old version number, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: sc.folder.Id,
|
||||
Version: 1,
|
||||
Kind: int64(Panel),
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 412, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel with an other kind, it should succeed but panel should not change",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: sc.folder.Id,
|
||||
Version: 1,
|
||||
Kind: int64(Variable),
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
sc.initialResult.Result.Type = "text"
|
||||
sc.initialResult.Result.Kind = int64(Panel)
|
||||
sc.initialResult.Result.Description = "A description"
|
||||
sc.initialResult.Result.Model = map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
"description": "A description",
|
||||
}
|
||||
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
|
||||
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
sc.initialResult.Result.Version = 2
|
||||
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
363
pkg/services/libraryelements/libraryelements_permissions_test.go
Normal file
363
pkg/services/libraryelements/libraryelements_permissions_test.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestLibraryElementPermissions(t *testing.T) {
|
||||
var defaultPermissions = []folderACLItem{}
|
||||
var adminOnlyPermissions = []folderACLItem{{models.ROLE_ADMIN, models.PERMISSION_EDIT}}
|
||||
var editorOnlyPermissions = []folderACLItem{{models.ROLE_EDITOR, models.PERMISSION_EDIT}}
|
||||
var editorAndViewerPermissions = []folderACLItem{{models.ROLE_EDITOR, models.PERMISSION_EDIT}, {models.ROLE_VIEWER, models.PERMISSION_EDIT}}
|
||||
var viewerOnlyPermissions = []folderACLItem{{models.ROLE_VIEWER, models.PERMISSION_EDIT}}
|
||||
var everyonePermissions = []folderACLItem{{models.ROLE_ADMIN, models.PERMISSION_EDIT}, {models.ROLE_EDITOR, models.PERMISSION_EDIT}, {models.ROLE_VIEWER, models.PERMISSION_EDIT}}
|
||||
var noPermissions = []folderACLItem{{models.ROLE_VIEWER, models.PERMISSION_VIEW}}
|
||||
var folderCases = [][]folderACLItem{
|
||||
defaultPermissions,
|
||||
adminOnlyPermissions,
|
||||
editorOnlyPermissions,
|
||||
editorAndViewerPermissions,
|
||||
viewerOnlyPermissions,
|
||||
everyonePermissions,
|
||||
noPermissions,
|
||||
}
|
||||
var defaultDesc = "default permissions"
|
||||
var adminOnlyDesc = "admin only permissions"
|
||||
var editorOnlyDesc = "editor only permissions"
|
||||
var editorAndViewerDesc = "editor and viewer permissions"
|
||||
var viewerOnlyDesc = "viewer only permissions"
|
||||
var everyoneDesc = "everyone has editor permissions"
|
||||
var noDesc = "everyone has view permissions"
|
||||
var accessCases = []struct {
|
||||
role models.RoleType
|
||||
items []folderACLItem
|
||||
desc string
|
||||
status int
|
||||
}{
|
||||
{models.ROLE_ADMIN, defaultPermissions, defaultDesc, 200},
|
||||
{models.ROLE_ADMIN, adminOnlyPermissions, adminOnlyDesc, 200},
|
||||
{models.ROLE_ADMIN, editorOnlyPermissions, editorOnlyDesc, 200},
|
||||
{models.ROLE_ADMIN, editorAndViewerPermissions, editorAndViewerDesc, 200},
|
||||
{models.ROLE_ADMIN, viewerOnlyPermissions, viewerOnlyDesc, 200},
|
||||
{models.ROLE_ADMIN, everyonePermissions, everyoneDesc, 200},
|
||||
{models.ROLE_ADMIN, noPermissions, noDesc, 200},
|
||||
{models.ROLE_EDITOR, defaultPermissions, defaultDesc, 200},
|
||||
{models.ROLE_EDITOR, adminOnlyPermissions, adminOnlyDesc, 403},
|
||||
{models.ROLE_EDITOR, editorOnlyPermissions, editorOnlyDesc, 200},
|
||||
{models.ROLE_EDITOR, editorAndViewerPermissions, editorAndViewerDesc, 200},
|
||||
{models.ROLE_EDITOR, viewerOnlyPermissions, viewerOnlyDesc, 403},
|
||||
{models.ROLE_EDITOR, everyonePermissions, everyoneDesc, 200},
|
||||
{models.ROLE_EDITOR, noPermissions, noDesc, 403},
|
||||
{models.ROLE_VIEWER, defaultPermissions, defaultDesc, 403},
|
||||
{models.ROLE_VIEWER, adminOnlyPermissions, adminOnlyDesc, 403},
|
||||
{models.ROLE_VIEWER, editorOnlyPermissions, editorOnlyDesc, 403},
|
||||
{models.ROLE_VIEWER, editorAndViewerPermissions, editorAndViewerDesc, 200},
|
||||
{models.ROLE_VIEWER, viewerOnlyPermissions, viewerOnlyDesc, 200},
|
||||
{models.ROLE_VIEWER, everyonePermissions, everyoneDesc, 200},
|
||||
{models.ROLE_VIEWER, noPermissions, noDesc, 403},
|
||||
}
|
||||
|
||||
for _, testCase := range accessCases {
|
||||
testScenario(t, fmt.Sprintf("When %s tries to create a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
command := getCreatePanelCommand(folder.Id, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to a folder with %s, it should return correct status", testCase.role, testCase.desc),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
fromFolder := createFolderWithACL(t, sc.sqlStore, "Everyone", sc.user, everyonePermissions)
|
||||
command := getCreatePanelCommand(fromFolder.Id, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(Panel)}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from a folder with %s, it should return correct status", testCase.role, testCase.desc),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
fromFolder := createFolderWithACL(t, sc.sqlStore, "Everyone", sc.user, testCase.items)
|
||||
command := getCreatePanelCommand(fromFolder.Id, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(Panel)}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
|
||||
cmd := getCreatePanelCommand(folder.Id, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, cmd)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
})
|
||||
}
|
||||
|
||||
var generalFolderCases = []struct {
|
||||
role models.RoleType
|
||||
status int
|
||||
}{
|
||||
{models.ROLE_ADMIN, 200},
|
||||
{models.ROLE_EDITOR, 200},
|
||||
{models.ROLE_VIEWER, 403},
|
||||
}
|
||||
|
||||
for _, testCase := range generalFolderCases {
|
||||
testScenario(t, fmt.Sprintf("When %s tries to create a library panel in the General folder, it should return correct status", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
command := getCreatePanelCommand(0, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to the General folder, it should return correct status", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
|
||||
command := getCreatePanelCommand(folder.Id, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
cmd := patchLibraryElementCommand{FolderID: 0, Version: 1, Kind: int64(Panel)}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from the General folder, it should return correct status", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
|
||||
command := getCreatePanelCommand(0, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
cmd := patchLibraryElementCommand{FolderID: folder.Id, Version: 1, Kind: int64(Panel)}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in the General folder, it should return correct status", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := getCreatePanelCommand(0, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, cmd)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
})
|
||||
}
|
||||
|
||||
var missingFolderCases = []struct {
|
||||
role models.RoleType
|
||||
}{
|
||||
{models.ROLE_ADMIN},
|
||||
{models.ROLE_EDITOR},
|
||||
{models.ROLE_VIEWER},
|
||||
}
|
||||
|
||||
for _, testCase := range missingFolderCases {
|
||||
testScenario(t, fmt.Sprintf("When %s tries to create a library panel in a folder that doesn't exist, it should fail", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
command := getCreatePanelCommand(-100, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
|
||||
command := getCreatePanelCommand(folder.Id, "Library Panel Name")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
cmd := patchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(Panel)}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
}
|
||||
|
||||
var getCases = []struct {
|
||||
role models.RoleType
|
||||
statuses []int
|
||||
}{
|
||||
{models.ROLE_ADMIN, []int{200, 200, 200, 200, 200, 200, 200}},
|
||||
{models.ROLE_EDITOR, []int{200, 404, 200, 200, 200, 200, 200}},
|
||||
{models.ROLE_VIEWER, []int{200, 404, 404, 200, 200, 200, 200}},
|
||||
}
|
||||
|
||||
for _, testCase := range getCases {
|
||||
testScenario(t, fmt.Sprintf("When %s tries to get a library panel, it should return correct response", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
var results []libraryElement
|
||||
for i, folderCase := range folderCases {
|
||||
folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase)
|
||||
cmd := getCreatePanelCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i))
|
||||
resp := sc.service.createHandler(sc.reqContext, cmd)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
result.Result.Meta.CreatedBy.Name = userInDbName
|
||||
result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
result.Result.Meta.UpdatedBy.Name = userInDbName
|
||||
result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar
|
||||
result.Result.Meta.FolderName = folder.Title
|
||||
result.Result.Meta.FolderUID = folder.Uid
|
||||
results = append(results, result.Result)
|
||||
}
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
for i, result := range results {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.UID})
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
require.Equal(t, testCase.statuses[i], resp.Status())
|
||||
}
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to get a library panel from General folder, it should return correct response", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := getCreatePanelCommand(0, "Library Panel in General Folder")
|
||||
resp := sc.service.createHandler(sc.reqContext, cmd)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
result.Result.Meta.CreatedBy.Name = userInDbName
|
||||
result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
result.Result.Meta.UpdatedBy.Name = userInDbName
|
||||
result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar
|
||||
result.Result.Meta.FolderName = "General"
|
||||
result.Result.Meta.FolderUID = ""
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.getHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var actual libraryElementResult
|
||||
err := json.Unmarshal(resp.Body(), &actual)
|
||||
require.NoError(t, err)
|
||||
if diff := cmp.Diff(result.Result, actual.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var getAllCases = []struct {
|
||||
role models.RoleType
|
||||
panels int
|
||||
folderIndexes []int
|
||||
}{
|
||||
{models.ROLE_ADMIN, 7, []int{0, 1, 2, 3, 4, 5, 6}},
|
||||
{models.ROLE_EDITOR, 6, []int{0, 2, 3, 4, 5, 6}},
|
||||
{models.ROLE_VIEWER, 5, []int{0, 3, 4, 5, 6}},
|
||||
}
|
||||
|
||||
for _, testCase := range getAllCases {
|
||||
testScenario(t, fmt.Sprintf("When %s tries to get all library panels, it should return correct response", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
var results []libraryElement
|
||||
for i, folderCase := range folderCases {
|
||||
folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase)
|
||||
cmd := getCreatePanelCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i))
|
||||
resp := sc.service.createHandler(sc.reqContext, cmd)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
result.Result.Meta.CreatedBy.Name = userInDbName
|
||||
result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
result.Result.Meta.UpdatedBy.Name = userInDbName
|
||||
result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar
|
||||
result.Result.Meta.FolderName = folder.Title
|
||||
result.Result.Meta.FolderUID = folder.Uid
|
||||
results = append(results, result.Result)
|
||||
}
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
resp := sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var actual libraryElementsSearch
|
||||
err := json.Unmarshal(resp.Body(), &actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testCase.panels, len(actual.Result.Elements))
|
||||
for _, folderIndex := range testCase.folderIndexes {
|
||||
var folderID = int64(folderIndex + 2) // testScenario creates one folder and general folder doesn't count
|
||||
var foundExists = false
|
||||
var foundResult libraryElement
|
||||
var actualExists = false
|
||||
var actualResult libraryElement
|
||||
for _, result := range results {
|
||||
if result.FolderID == folderID {
|
||||
foundExists = true
|
||||
foundResult = result
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Equal(t, foundExists, true)
|
||||
|
||||
for _, result := range actual.Result.Elements {
|
||||
if result.FolderID == folderID {
|
||||
actualExists = true
|
||||
actualResult = result
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Equal(t, actualExists, true)
|
||||
|
||||
if diff := cmp.Diff(foundResult, actualResult, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
testScenario(t, fmt.Sprintf("When %s tries to get all library panels from General folder, it should return correct response", testCase.role),
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := getCreatePanelCommand(0, "Library Panel in General Folder")
|
||||
resp := sc.service.createHandler(sc.reqContext, cmd)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
result.Result.Meta.CreatedBy.Name = userInDbName
|
||||
result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
|
||||
result.Result.Meta.UpdatedBy.Name = userInDbName
|
||||
result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar
|
||||
result.Result.Meta.FolderName = "General"
|
||||
sc.reqContext.SignedInUser.OrgRole = testCase.role
|
||||
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var actual libraryElementsSearch
|
||||
err := json.Unmarshal(resp.Body(), &actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(actual.Result.Elements))
|
||||
if diff := cmp.Diff(result.Result, actual.Result.Elements[0], getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
353
pkg/services/libraryelements/libraryelements_test.go
Normal file
353
pkg/services/libraryelements/libraryelements_test.go
Normal file
@@ -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()
|
||||
}),
|
||||
}
|
||||
}
|
||||
190
pkg/services/libraryelements/models.go
Normal file
190
pkg/services/libraryelements/models.go
Normal file
@@ -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
|
||||
}
|
||||
108
pkg/services/libraryelements/writers.go
Normal file
108
pkg/services/libraryelements/writers.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func writePerPageSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
|
||||
if query.perPage != 0 {
|
||||
offset := query.perPage * (query.page - 1)
|
||||
builder.Write(sqlStore.Dialect.LimitOffset(int64(query.perPage), int64(offset)))
|
||||
}
|
||||
}
|
||||
|
||||
func writeKindSQL(query searchLibraryElementsQuery, builder *sqlstore.SQLBuilder) {
|
||||
if LibraryElementKind(query.kind) == Panel || LibraryElementKind(query.kind) == Variable {
|
||||
builder.Write(" AND le.kind = ?", query.kind)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTypeFilterSQL(typeFilter []string, builder *sqlstore.SQLBuilder) {
|
||||
if len(typeFilter) > 0 {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
sql.WriteString(` AND le.type IN (?` + strings.Repeat(",?", len(typeFilter)-1) + ")")
|
||||
for _, filter := range typeFilter {
|
||||
params = append(params, filter)
|
||||
}
|
||||
builder.Write(sql.String(), params...)
|
||||
}
|
||||
}
|
||||
|
||||
func writeSearchStringSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
|
||||
if len(strings.TrimSpace(query.searchString)) > 0 {
|
||||
builder.Write(" AND (le.name "+sqlStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||
builder.Write(" OR le.description "+sqlStore.Dialect.LikeStr()+" ?)", "%"+query.searchString+"%")
|
||||
}
|
||||
}
|
||||
|
||||
func writeExcludeSQL(query searchLibraryElementsQuery, builder *sqlstore.SQLBuilder) {
|
||||
if len(strings.TrimSpace(query.excludeUID)) > 0 {
|
||||
builder.Write(" AND le.uid <> ?", query.excludeUID)
|
||||
}
|
||||
}
|
||||
|
||||
type FolderFilter struct {
|
||||
includeGeneralFolder bool
|
||||
folderIDs []string
|
||||
parseError error
|
||||
}
|
||||
|
||||
func parseFolderFilter(query searchLibraryElementsQuery) FolderFilter {
|
||||
folderIDs := make([]string, 0)
|
||||
if len(strings.TrimSpace(query.folderFilter)) == 0 {
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: true,
|
||||
folderIDs: folderIDs,
|
||||
parseError: nil,
|
||||
}
|
||||
}
|
||||
|
||||
includeGeneralFolder := false
|
||||
folderIDs = strings.Split(query.folderFilter, ",")
|
||||
for _, filter := range folderIDs {
|
||||
folderID, err := strconv.ParseInt(filter, 10, 64)
|
||||
if err != nil {
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: false,
|
||||
folderIDs: folderIDs,
|
||||
parseError: err,
|
||||
}
|
||||
}
|
||||
if isGeneralFolder(folderID) {
|
||||
includeGeneralFolder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: includeGeneralFolder,
|
||||
folderIDs: folderIDs,
|
||||
parseError: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FolderFilter) writeFolderFilterSQL(includeGeneral bool, builder *sqlstore.SQLBuilder) error {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
for _, filter := range f.folderIDs {
|
||||
folderID, err := strconv.ParseInt(filter, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !includeGeneral && isGeneralFolder(folderID) {
|
||||
continue
|
||||
}
|
||||
params = append(params, filter)
|
||||
}
|
||||
if len(params) > 0 {
|
||||
sql.WriteString(` AND le.folder_id IN (?` + strings.Repeat(",?", len(params)-1) + ")")
|
||||
builder.Write(sql.String(), params...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user