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:
Hugo Häggmark 2021-05-11 07:10:19 +02:00 committed by GitHub
parent 9b12e79f3e
commit f1b2c750e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2758 additions and 2445 deletions

View File

@ -250,10 +250,10 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
}
if hs.Cfg.IsPanelLibraryEnabled() {
// disconnect all library panels for this dashboard
err := hs.LibraryPanelService.DisconnectLibraryPanelsForDashboard(c, dash)
// disconnect all library elements for this dashboard
err := hs.LibraryElementService.DisconnectElementsFromDashboard(c, dash.Id)
if err != nil {
hs.log.Error("Failed to disconnect library panels", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
}
}

View File

@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
@ -89,10 +89,10 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext, cmd models.UpdateFolder
func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
if hs.Cfg.IsPanelLibraryEnabled() {
err := hs.LibraryPanelService.DeleteLibraryPanelsInFolder(c, c.Params(":uid"))
err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c, c.Params(":uid"))
if err != nil {
if errors.Is(err, librarypanels.ErrFolderHasConnectedLibraryPanels) {
return response.Error(403, "Folder could not be deleted because it contains linked library panels", err)
if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) {
return response.Error(403, "Folder could not be deleted because it contains library elements in use", err)
}
return ToFolderErrorResponse(err)
}

View File

@ -34,6 +34,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/live/pushhttp"
@ -48,8 +49,8 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
macaron "gopkg.in/macaron.v1"
@ -97,6 +98,7 @@ type HTTPServer struct {
ContextHandler *contexthandler.ContextHandler `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
LibraryPanelService *librarypanels.LibraryPanelService `inject:""`
LibraryElementService *libraryelements.LibraryElementService `inject:""`
DataService *tsdb.Service `inject:""`
PluginDashboardService *plugindashboards.Service `inject:""`
AlertEngine *alerting.AlertEngine `inject:""`

View 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)
}

View 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
})
}

View File

@ -1,4 +1,4 @@
package librarypanels
package libraryelements
import (
"github.com/grafana/grafana/pkg/models"
@ -10,7 +10,19 @@ func isGeneralFolder(folderID int64) bool {
return folderID == 0
}
func (lps *LibraryPanelService) requirePermissionsOnFolder(user *models.SignedInUser, folderID int64) error {
func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) error {
kind := LibraryElementKind(kindAsInt)
switch kind {
case Panel:
return nil
case Variable:
return nil
default:
return errLibraryElementUnSupportedElementKind
}
}
func (l *LibraryElementService) requirePermissionsOnFolder(user *models.SignedInUser, folderID int64) error {
if isGeneralFolder(folderID) && user.HasRole(models.ROLE_EDITOR) {
return nil
}
@ -19,7 +31,7 @@ func (lps *LibraryPanelService) requirePermissionsOnFolder(user *models.SignedIn
return models.ErrFolderAccessDenied
}
s := dashboards.NewFolderService(user.OrgId, user, lps.SQLStore)
s := dashboards.NewFolderService(user.OrgId, user, l.SQLStore)
folder, err := s.GetFolderByID(folderID)
if err != nil {
return err

View 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]))
}

View File

@ -1,4 +1,4 @@
package librarypanels
package libraryelements
import (
"testing"
@ -7,23 +7,24 @@ import (
"github.com/stretchr/testify/require"
)
func TestCreateLibraryPanel(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to create a library panel that already exists, it should fail",
func TestCreateLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to create a library panel that already exists, it should fail",
func(t *testing.T, sc scenarioContext) {
command := getCreateCommand(sc.folder.Id, "Text - Library Panel")
command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel")
resp := sc.service.createHandler(sc.reqContext, command)
require.Equal(t, 400, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to create a library panel that does not exists, it should succeed",
scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists, it should succeed",
func(t *testing.T, sc scenarioContext) {
var expected = libraryPanelResult{
Result: libraryPanel{
var expected = libraryElementResult{
Result: libraryElement{
ID: 1,
OrgID: 1,
FolderID: 1,
UID: sc.initialResult.Result.UID,
Name: "Text - Library Panel",
Kind: int64(Panel),
Type: "text",
Description: "A description",
Model: map[string]interface{}{
@ -34,20 +35,19 @@ func TestCreateLibraryPanel(t *testing.T) {
"type": "text",
},
Version: 1,
Meta: LibraryPanelDTOMeta{
CanEdit: true,
ConnectedDashboards: 0,
Created: sc.initialResult.Result.Meta.Created,
Updated: sc.initialResult.Result.Meta.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
Meta: LibraryElementDTOMeta{
Connections: 0,
Created: sc.initialResult.Result.Meta.Created,
Updated: sc.initialResult.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: "signed_in_user",
AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
},
UpdatedBy: LibraryPanelDTOMetaUser{
UpdatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: "signed_in_user",
AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
},
},
},
@ -59,16 +59,17 @@ func TestCreateLibraryPanel(t *testing.T) {
testScenario(t, "When an admin tries to create a library panel where name and panel title differ, it should update panel title",
func(t *testing.T, sc scenarioContext) {
command := getCreateCommand(1, "Library Panel Name")
command := getCreatePanelCommand(1, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
var result = validateAndUnMarshalResponse(t, resp)
var expected = libraryPanelResult{
Result: libraryPanel{
var expected = libraryElementResult{
Result: libraryElement{
ID: 1,
OrgID: 1,
FolderID: 1,
UID: result.Result.UID,
Name: "Library Panel Name",
Kind: int64(Panel),
Type: "text",
Description: "A description",
Model: map[string]interface{}{
@ -79,20 +80,19 @@ func TestCreateLibraryPanel(t *testing.T) {
"type": "text",
},
Version: 1,
Meta: LibraryPanelDTOMeta{
CanEdit: true,
ConnectedDashboards: 0,
Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
Meta: LibraryElementDTOMeta{
Connections: 0,
Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: "signed_in_user",
AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
},
UpdatedBy: LibraryPanelDTOMetaUser{
UpdatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: "signed_in_user",
AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
},
},
},

View 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())
})
}

View 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())
})
}

View File

@ -1,4 +1,4 @@
package librarypanels
package libraryelements
import (
"testing"
@ -7,26 +7,19 @@ import (
"github.com/stretchr/testify/require"
)
func TestPatchLibraryPanel(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel that does not exist, it should fail",
func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryPanelCommand{}
cmd := patchLibraryElementCommand{Kind: int64(Panel)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
resp := sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel that exists, it should succeed",
scenarioWithPanel(t, "When an admin tries to patch a library panel that exists, it should succeed",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "2"})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: newFolder.Id,
Name: "Panel - New name",
Model: []byte(`
@ -38,19 +31,21 @@ func TestPatchLibraryPanel(t *testing.T) {
"type": "graph"
}
`),
Kind: int64(Panel),
Version: 1,
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd)
resp := sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 200, resp.Status())
var result = validateAndUnMarshalResponse(t, resp)
var expected = libraryPanelResult{
Result: libraryPanel{
var expected = libraryElementResult{
Result: libraryElement{
ID: 1,
OrgID: 1,
FolderID: newFolder.Id,
UID: sc.initialResult.Result.UID,
Name: "Panel - New name",
Kind: int64(Panel),
Type: "graph",
Description: "An updated description",
Model: map[string]interface{}{
@ -61,20 +56,19 @@ func TestPatchLibraryPanel(t *testing.T) {
"type": "graph",
},
Version: 2,
Meta: LibraryPanelDTOMeta{
CanEdit: true,
ConnectedDashboards: 2,
Created: sc.initialResult.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
Meta: LibraryElementDTOMeta{
Connections: 0,
Created: sc.initialResult.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: UserInDbName,
AvatarUrl: UserInDbAvatar,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
UpdatedBy: LibraryPanelDTOMetaUser{
UpdatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: "signed_in_user",
AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b",
},
},
},
@ -84,11 +78,12 @@ func TestPatchLibraryPanel(t *testing.T) {
}
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result",
scenarioWithPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result",
func(t *testing.T, sc scenarioContext) {
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: newFolder.Id,
Kind: int64(Panel),
Version: 1,
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -96,27 +91,28 @@ func TestPatchLibraryPanel(t *testing.T) {
require.Equal(t, 200, resp.Status())
var result = validateAndUnMarshalResponse(t, resp)
sc.initialResult.Result.FolderID = newFolder.Id
sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with name only, it should change name successfully, sync title and return correct result",
scenarioWithPanel(t, "When an admin tries to patch a library panel with name only, it should change name successfully, sync title and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: -1,
Name: "New Name",
Kind: int64(Panel),
Version: 1,
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.patchHandler(sc.reqContext, cmd)
var result = validateAndUnMarshalResponse(t, resp)
sc.initialResult.Result.Name = "New Name"
sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Model["title"] = "New Name"
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
@ -124,11 +120,12 @@ func TestPatchLibraryPanel(t *testing.T) {
}
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with model only, it should change model successfully, sync name, type and description fields and return correct result",
scenarioWithPanel(t, "When an admin tries to patch a library panel with model only, it should change model successfully, sync name, type and description fields and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: -1,
Model: []byte(`{ "title": "New Model Title", "name": "New Model Name", "type":"graph", "description": "New description" }`),
Kind: int64(Panel),
Version: 1,
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -142,19 +139,20 @@ func TestPatchLibraryPanel(t *testing.T) {
"type": "graph",
"description": "New description",
}
sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with model.description only, it should change model successfully, sync name, type and description fields and return correct result",
scenarioWithPanel(t, "When an admin tries to patch a library panel with model.description only, it should change model successfully, sync name, type and description fields and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: -1,
Model: []byte(`{ "description": "New description" }`),
Kind: int64(Panel),
Version: 1,
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -167,19 +165,20 @@ func TestPatchLibraryPanel(t *testing.T) {
"type": "text",
"description": "New description",
}
sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with model.type only, it should change model successfully, sync name, type and description fields and return correct result",
scenarioWithPanel(t, "When an admin tries to patch a library panel with model.type only, it should change model successfully, sync name, type and description fields and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: -1,
Model: []byte(`{ "type": "graph" }`),
Kind: int64(Panel),
Version: 1,
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -192,64 +191,67 @@ func TestPatchLibraryPanel(t *testing.T) {
"type": "graph",
"description": "A description",
}
sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result",
scenarioWithPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryPanelCommand{FolderID: -1, Version: 1}
cmd := patchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(Panel)}
sc.reqContext.UserId = 2
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.patchHandler(sc.reqContext, cmd)
var result = validateAndUnMarshalResponse(t, resp)
sc.initialResult.Result.Meta.UpdatedBy.ID = int64(2)
sc.initialResult.Result.Meta.CreatedBy.Name = UserInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should fail",
scenarioWithPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should fail",
func(t *testing.T, sc scenarioContext) {
command := getCreateCommand(sc.folder.Id, "Another Panel")
command := getCreatePanelCommand(sc.folder.Id, "Another Panel")
resp := sc.service.createHandler(sc.reqContext, command)
var result = validateAndUnMarshalResponse(t, resp)
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
Name: "Text - Library Panel",
Version: 1,
Kind: int64(Panel),
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 400, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail",
scenarioWithPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail",
func(t *testing.T, sc scenarioContext) {
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
command := getCreateCommand(newFolder.Id, "Text - Library Panel")
command := getCreatePanelCommand(newFolder.Id, "Text - Library Panel")
resp := sc.service.createHandler(sc.reqContext, command)
var result = validateAndUnMarshalResponse(t, resp)
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: 1,
Version: 1,
Kind: int64(Panel),
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 400, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel in another org, it should fail",
scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: sc.folder.Id,
Version: 1,
Kind: int64(Panel),
}
sc.reqContext.OrgId = 2
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -257,11 +259,12 @@ func TestPatchLibraryPanel(t *testing.T) {
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with an old version number, it should fail",
scenarioWithPanel(t, "When an admin tries to patch a library panel with an old version number, it should fail",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryPanelCommand{
cmd := patchLibraryElementCommand{
FolderID: sc.folder.Id,
Version: 1,
Kind: int64(Panel),
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.patchHandler(sc.reqContext, cmd)
@ -269,4 +272,33 @@ func TestPatchLibraryPanel(t *testing.T) {
resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 412, resp.Status())
})
scenarioWithPanel(t, "When an admin tries to patch a library panel with an other kind, it should succeed but panel should not change",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
FolderID: sc.folder.Id,
Version: 1,
Kind: int64(Variable),
}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 200, resp.Status())
var result = validateAndUnMarshalResponse(t, resp)
sc.initialResult.Result.Type = "text"
sc.initialResult.Result.Kind = int64(Panel)
sc.initialResult.Result.Description = "A description"
sc.initialResult.Result.Model = map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}",
"id": float64(1),
"title": "Text - Library Panel",
"type": "text",
"description": "A description",
}
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
}

View File

@ -1,18 +1,17 @@
package librarypanels
package libraryelements
import (
"encoding/json"
"fmt"
"strconv"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/require"
)
func TestLibraryPanelPermissions(t *testing.T) {
func TestLibraryElementPermissions(t *testing.T) {
var defaultPermissions = []folderACLItem{}
var adminOnlyPermissions = []folderACLItem{{models.ROLE_ADMIN, models.PERMISSION_EDIT}}
var editorOnlyPermissions = []folderACLItem{{models.ROLE_EDITOR, models.PERMISSION_EDIT}}
@ -71,7 +70,7 @@ func TestLibraryPanelPermissions(t *testing.T) {
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
sc.reqContext.SignedInUser.OrgRole = testCase.role
command := getCreateCommand(folder.Id, "Library Panel Name")
command := getCreatePanelCommand(folder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
require.Equal(t, testCase.status, resp.Status())
})
@ -79,13 +78,13 @@ func TestLibraryPanelPermissions(t *testing.T) {
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to a folder with %s, it should return correct status", testCase.role, testCase.desc),
func(t *testing.T, sc scenarioContext) {
fromFolder := createFolderWithACL(t, sc.sqlStore, "Everyone", sc.user, everyonePermissions)
command := getCreateCommand(fromFolder.Id, "Library Panel Name")
command := getCreatePanelCommand(fromFolder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
result := validateAndUnMarshalResponse(t, resp)
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryPanelCommand{FolderID: toFolder.Id, Version: 1}
cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(Panel)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, testCase.status, resp.Status())
@ -94,13 +93,13 @@ func TestLibraryPanelPermissions(t *testing.T) {
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from a folder with %s, it should return correct status", testCase.role, testCase.desc),
func(t *testing.T, sc scenarioContext) {
fromFolder := createFolderWithACL(t, sc.sqlStore, "Everyone", sc.user, testCase.items)
command := getCreateCommand(fromFolder.Id, "Library Panel Name")
command := getCreatePanelCommand(fromFolder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
result := validateAndUnMarshalResponse(t, resp)
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryPanelCommand{FolderID: toFolder.Id, Version: 1}
cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(Panel)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, testCase.status, resp.Status())
@ -109,7 +108,7 @@ func TestLibraryPanelPermissions(t *testing.T) {
testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc),
func(t *testing.T, sc scenarioContext) {
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
cmd := getCreateCommand(folder.Id, "Library Panel Name")
cmd := getCreatePanelCommand(folder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
@ -118,56 +117,6 @@ func TestLibraryPanelPermissions(t *testing.T) {
resp = sc.service.deleteHandler(sc.reqContext)
require.Equal(t, testCase.status, resp.Status())
})
testScenario(t, fmt.Sprintf("When %s tries to connect a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc),
func(t *testing.T, sc scenarioContext) {
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id)
cmd := getCreateCommand(folder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, testCase.status, resp.Status())
})
testScenario(t, fmt.Sprintf("When %s tries to disconnect a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc),
func(t *testing.T, sc scenarioContext) {
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id)
cmd := getCreateCommand(folder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
sc.reqContext.SignedInUser.OrgRole = testCase.role
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)})
resp = sc.service.disconnectHandler(sc.reqContext)
require.Equal(t, testCase.status, resp.Status())
})
testScenario(t, fmt.Sprintf("When %s tries to delete all library panels in a folder with %s, it should return correct status", testCase.role, testCase.desc),
func(t *testing.T, sc scenarioContext) {
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
cmd := getCreateCommand(folder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, cmd)
validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
err := sc.service.DeleteLibraryPanelsInFolder(sc.reqContext, folder.Uid)
switch testCase.status {
case 200:
require.NoError(t, err)
case 403:
require.EqualError(t, err, models.ErrFolderAccessDenied.Error())
default:
t.Fatalf("Unrecognized test case status %d", testCase.status)
}
})
}
var generalFolderCases = []struct {
@ -184,7 +133,7 @@ func TestLibraryPanelPermissions(t *testing.T) {
func(t *testing.T, sc scenarioContext) {
sc.reqContext.SignedInUser.OrgRole = testCase.role
command := getCreateCommand(0, "Library Panel Name")
command := getCreatePanelCommand(0, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
require.Equal(t, testCase.status, resp.Status())
})
@ -192,12 +141,12 @@ func TestLibraryPanelPermissions(t *testing.T) {
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to the General folder, it should return correct status", testCase.role),
func(t *testing.T, sc scenarioContext) {
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
command := getCreateCommand(folder.Id, "Library Panel Name")
command := getCreatePanelCommand(folder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryPanelCommand{FolderID: 0, Version: 1}
cmd := patchLibraryElementCommand{FolderID: 0, Version: 1, Kind: int64(Panel)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, testCase.status, resp.Status())
@ -206,12 +155,12 @@ func TestLibraryPanelPermissions(t *testing.T) {
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from the General folder, it should return correct status", testCase.role),
func(t *testing.T, sc scenarioContext) {
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
command := getCreateCommand(0, "Library Panel Name")
command := getCreatePanelCommand(0, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryPanelCommand{FolderID: folder.Id, Version: 1}
cmd := patchLibraryElementCommand{FolderID: folder.Id, Version: 1, Kind: int64(Panel)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, testCase.status, resp.Status())
@ -219,7 +168,7 @@ func TestLibraryPanelPermissions(t *testing.T) {
testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in the General folder, it should return correct status", testCase.role),
func(t *testing.T, sc scenarioContext) {
cmd := getCreateCommand(0, "Library Panel Name")
cmd := getCreatePanelCommand(0, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
@ -228,57 +177,6 @@ func TestLibraryPanelPermissions(t *testing.T) {
resp = sc.service.deleteHandler(sc.reqContext)
require.Equal(t, testCase.status, resp.Status())
})
testScenario(t, fmt.Sprintf("When %s tries to connect a library panel in the General folder, it should return correct status", testCase.role),
func(t *testing.T, sc scenarioContext) {
dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0)
cmd := getCreateCommand(0, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, testCase.status, resp.Status())
})
testScenario(t, fmt.Sprintf("When %s tries to disconnect a library panel in the General folder, it should return correct status", testCase.role),
func(t *testing.T, sc scenarioContext) {
dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0)
cmd := getCreateCommand(0, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
sc.reqContext.SignedInUser.OrgRole = testCase.role
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)})
resp = sc.service.disconnectHandler(sc.reqContext)
require.Equal(t, testCase.status, resp.Status())
})
testScenario(t, fmt.Sprintf("When %s tries to get connected dashboards in the General folder for a library panel in the General folder, it should return correct status", testCase.role),
func(t *testing.T, sc scenarioContext) {
dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0)
cmd := getCreateCommand(0, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
sc.reqContext.SignedInUser.OrgRole = testCase.role
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var dashResult libraryPanelDashboardsResult
err := json.Unmarshal(resp.Body(), &dashResult)
require.NoError(t, err)
require.Equal(t, 200, resp.Status())
require.Equal(t, 1, len(dashResult.Result))
require.Equal(t, dashboard.Id, dashResult.Result[0])
})
}
var missingFolderCases = []struct {
@ -294,7 +192,7 @@ func TestLibraryPanelPermissions(t *testing.T) {
func(t *testing.T, sc scenarioContext) {
sc.reqContext.SignedInUser.OrgRole = testCase.role
command := getCreateCommand(-100, "Library Panel Name")
command := getCreatePanelCommand(-100, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
require.Equal(t, 404, resp.Status())
})
@ -302,18 +200,78 @@ func TestLibraryPanelPermissions(t *testing.T) {
testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", testCase.role),
func(t *testing.T, sc scenarioContext) {
folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
command := getCreateCommand(folder.Id, "Library Panel Name")
command := getCreatePanelCommand(folder.Id, "Library Panel Name")
resp := sc.service.createHandler(sc.reqContext, command)
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryPanelCommand{FolderID: -100, Version: 1}
cmd := patchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(Panel)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 404, resp.Status())
})
}
var getCases = []struct {
role models.RoleType
statuses []int
}{
{models.ROLE_ADMIN, []int{200, 200, 200, 200, 200, 200, 200}},
{models.ROLE_EDITOR, []int{200, 404, 200, 200, 200, 200, 200}},
{models.ROLE_VIEWER, []int{200, 404, 404, 200, 200, 200, 200}},
}
for _, testCase := range getCases {
testScenario(t, fmt.Sprintf("When %s tries to get a library panel, it should return correct response", testCase.role),
func(t *testing.T, sc scenarioContext) {
var results []libraryElement
for i, folderCase := range folderCases {
folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase)
cmd := getCreatePanelCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i))
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
result.Result.Meta.CreatedBy.Name = userInDbName
result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
result.Result.Meta.UpdatedBy.Name = userInDbName
result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar
result.Result.Meta.FolderName = folder.Title
result.Result.Meta.FolderUID = folder.Uid
results = append(results, result.Result)
}
sc.reqContext.SignedInUser.OrgRole = testCase.role
for i, result := range results {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.UID})
resp := sc.service.getHandler(sc.reqContext)
require.Equal(t, testCase.statuses[i], resp.Status())
}
})
testScenario(t, fmt.Sprintf("When %s tries to get a library panel from General folder, it should return correct response", testCase.role),
func(t *testing.T, sc scenarioContext) {
cmd := getCreatePanelCommand(0, "Library Panel in General Folder")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
result.Result.Meta.CreatedBy.Name = userInDbName
result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
result.Result.Meta.UpdatedBy.Name = userInDbName
result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar
result.Result.Meta.FolderName = "General"
result.Result.Meta.FolderUID = ""
sc.reqContext.SignedInUser.OrgRole = testCase.role
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.getHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var actual libraryElementResult
err := json.Unmarshal(resp.Body(), &actual)
require.NoError(t, err)
if diff := cmp.Diff(result.Result, actual.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
}
var getAllCases = []struct {
role models.RoleType
panels int
@ -327,16 +285,16 @@ func TestLibraryPanelPermissions(t *testing.T) {
for _, testCase := range getAllCases {
testScenario(t, fmt.Sprintf("When %s tries to get all library panels, it should return correct response", testCase.role),
func(t *testing.T, sc scenarioContext) {
var results []libraryPanel
var results []libraryElement
for i, folderCase := range folderCases {
folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase)
cmd := getCreateCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i))
cmd := getCreatePanelCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i))
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
result.Result.Meta.CreatedBy.Name = UserInDbName
result.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
result.Result.Meta.UpdatedBy.Name = UserInDbName
result.Result.Meta.UpdatedBy.AvatarUrl = UserInDbAvatar
result.Result.Meta.CreatedBy.Name = userInDbName
result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
result.Result.Meta.UpdatedBy.Name = userInDbName
result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar
result.Result.Meta.FolderName = folder.Title
result.Result.Meta.FolderUID = folder.Uid
results = append(results, result.Result)
@ -345,29 +303,33 @@ func TestLibraryPanelPermissions(t *testing.T) {
resp := sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var actual libraryPanelsSearch
var actual libraryElementsSearch
err := json.Unmarshal(resp.Body(), &actual)
require.NoError(t, err)
require.Equal(t, testCase.panels, len(actual.Result.LibraryPanels))
require.Equal(t, testCase.panels, len(actual.Result.Elements))
for _, folderIndex := range testCase.folderIndexes {
var folderID = int64(folderIndex + 2) // testScenario creates one folder and general folder doesn't count
var foundResult libraryPanel
var actualResult libraryPanel
var foundExists = false
var foundResult libraryElement
var actualExists = false
var actualResult libraryElement
for _, result := range results {
if result.FolderID == folderID {
foundExists = true
foundResult = result
break
}
}
require.NotEmpty(t, foundResult)
require.Equal(t, foundExists, true)
for _, result := range actual.Result.LibraryPanels {
for _, result := range actual.Result.Elements {
if result.FolderID == folderID {
actualExists = true
actualResult = result
break
}
}
require.NotEmpty(t, actualResult)
require.Equal(t, actualExists, true)
if diff := cmp.Diff(foundResult, actualResult, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@ -377,107 +339,23 @@ func TestLibraryPanelPermissions(t *testing.T) {
testScenario(t, fmt.Sprintf("When %s tries to get all library panels from General folder, it should return correct response", testCase.role),
func(t *testing.T, sc scenarioContext) {
cmd := getCreateCommand(0, "Library Panel in General Folder")
cmd := getCreatePanelCommand(0, "Library Panel in General Folder")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
result.Result.Meta.CreatedBy.Name = UserInDbName
result.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
result.Result.Meta.UpdatedBy.Name = UserInDbName
result.Result.Meta.UpdatedBy.AvatarUrl = UserInDbAvatar
result.Result.Meta.CreatedBy.Name = userInDbName
result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
result.Result.Meta.UpdatedBy.Name = userInDbName
result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar
result.Result.Meta.FolderName = "General"
sc.reqContext.SignedInUser.OrgRole = testCase.role
resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var actual libraryPanelsSearch
var actual libraryElementsSearch
err := json.Unmarshal(resp.Body(), &actual)
require.NoError(t, err)
require.Equal(t, 1, len(actual.Result.LibraryPanels))
if diff := cmp.Diff(result.Result, actual.Result.LibraryPanels[0], getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
testScenario(t, fmt.Sprintf("When %s tries to get connected dashboards for a library panel, it should return correct connected dashboard IDs", testCase.role),
func(t *testing.T, sc scenarioContext) {
cmd := getCreateCommand(0, "Library Panel in General Folder")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
for i, folderCase := range folderCases {
folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase)
dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
}
sc.reqContext.SignedInUser.OrgRole = testCase.role
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var dashResult libraryPanelDashboardsResult
err := json.Unmarshal(resp.Body(), &dashResult)
require.NoError(t, err)
require.Equal(t, testCase.panels, len(dashResult.Result))
})
}
var getCases = []struct {
role models.RoleType
statuses []int
}{
{models.ROLE_ADMIN, []int{200, 200, 200, 200, 200, 200, 200}},
{models.ROLE_EDITOR, []int{200, 404, 200, 200, 200, 200, 200}},
{models.ROLE_VIEWER, []int{200, 404, 404, 200, 200, 200, 200}},
}
for _, testCase := range getCases {
testScenario(t, fmt.Sprintf("When %s tries to get a library panel, it should return correct response", testCase.role),
func(t *testing.T, sc scenarioContext) {
var results []libraryPanel
for i, folderCase := range folderCases {
folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase)
cmd := getCreateCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i))
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
result.Result.Meta.CreatedBy.Name = UserInDbName
result.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
result.Result.Meta.UpdatedBy.Name = UserInDbName
result.Result.Meta.UpdatedBy.AvatarUrl = UserInDbAvatar
result.Result.Meta.FolderName = folder.Title
result.Result.Meta.FolderUID = folder.Uid
results = append(results, result.Result)
}
sc.reqContext.SignedInUser.OrgRole = testCase.role
for i, result := range results {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.UID})
resp := sc.service.getHandler(sc.reqContext)
require.Equal(t, testCase.statuses[i], resp.Status())
}
})
testScenario(t, fmt.Sprintf("When %s tries to get a library panel from General folder, it should return correct response", testCase.role),
func(t *testing.T, sc scenarioContext) {
cmd := getCreateCommand(0, "Library Panel in General Folder")
resp := sc.service.createHandler(sc.reqContext, cmd)
result := validateAndUnMarshalResponse(t, resp)
result.Result.Meta.CreatedBy.Name = UserInDbName
result.Result.Meta.CreatedBy.AvatarUrl = UserInDbAvatar
result.Result.Meta.UpdatedBy.Name = UserInDbName
result.Result.Meta.UpdatedBy.AvatarUrl = UserInDbAvatar
result.Result.Meta.FolderName = "General"
result.Result.Meta.FolderUID = ""
sc.reqContext.SignedInUser.OrgRole = testCase.role
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.getHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var actual libraryPanelResult
err := json.Unmarshal(resp.Body(), &actual)
require.NoError(t, err)
if diff := cmp.Diff(result.Result, actual.Result, getCompareOptions()...); diff != "" {
require.Equal(t, 1, len(actual.Result.Elements))
if diff := cmp.Diff(result.Result, actual.Result.Elements[0], getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})

View 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()
}),
}
}

View 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
}

View File

@ -1,4 +1,4 @@
package librarypanels
package libraryelements
import (
"bytes"
@ -8,35 +8,41 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore"
)
func writePerPageSQL(query searchLibraryPanelsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
func writePerPageSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
if query.perPage != 0 {
offset := query.perPage * (query.page - 1)
builder.Write(sqlStore.Dialect.LimitOffset(int64(query.perPage), int64(offset)))
}
}
func writePanelFilterSQL(panelFilter []string, builder *sqlstore.SQLBuilder) {
if len(panelFilter) > 0 {
func writeKindSQL(query searchLibraryElementsQuery, builder *sqlstore.SQLBuilder) {
if LibraryElementKind(query.kind) == Panel || LibraryElementKind(query.kind) == Variable {
builder.Write(" AND le.kind = ?", query.kind)
}
}
func writeTypeFilterSQL(typeFilter []string, builder *sqlstore.SQLBuilder) {
if len(typeFilter) > 0 {
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(` AND lp.type IN (?` + strings.Repeat(",?", len(panelFilter)-1) + ")")
for _, filter := range panelFilter {
sql.WriteString(` AND le.type IN (?` + strings.Repeat(",?", len(typeFilter)-1) + ")")
for _, filter := range typeFilter {
params = append(params, filter)
}
builder.Write(sql.String(), params...)
}
}
func writeSearchStringSQL(query searchLibraryPanelsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
func writeSearchStringSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
if len(strings.TrimSpace(query.searchString)) > 0 {
builder.Write(" AND (lp.name "+sqlStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
builder.Write(" OR lp.description "+sqlStore.Dialect.LikeStr()+" ?)", "%"+query.searchString+"%")
builder.Write(" AND (le.name "+sqlStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
builder.Write(" OR le.description "+sqlStore.Dialect.LikeStr()+" ?)", "%"+query.searchString+"%")
}
}
func writeExcludeSQL(query searchLibraryPanelsQuery, builder *sqlstore.SQLBuilder) {
func writeExcludeSQL(query searchLibraryElementsQuery, builder *sqlstore.SQLBuilder) {
if len(strings.TrimSpace(query.excludeUID)) > 0 {
builder.Write(" AND lp.uid <> ?", query.excludeUID)
builder.Write(" AND le.uid <> ?", query.excludeUID)
}
}
@ -46,8 +52,8 @@ type FolderFilter struct {
parseError error
}
func parseFolderFilter(query searchLibraryPanelsQuery) FolderFilter {
var folderIDs []string
func parseFolderFilter(query searchLibraryElementsQuery) FolderFilter {
folderIDs := make([]string, 0)
if len(strings.TrimSpace(query.folderFilter)) == 0 {
return FolderFilter{
includeGeneralFolder: true,
@ -94,7 +100,7 @@ func (f *FolderFilter) writeFolderFilterSQL(includeGeneral bool, builder *sqlsto
params = append(params, filter)
}
if len(params) > 0 {
sql.WriteString(` AND lp.folder_id IN (?` + strings.Repeat(",?", len(params)-1) + ")")
sql.WriteString(` AND le.folder_id IN (?` + strings.Repeat(",?", len(params)-1) + ")")
builder.Write(sql.String(), params...)
}

View File

@ -1,144 +0,0 @@
package librarypanels
import (
"errors"
"github.com/go-macaron/binding"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
func (lps *LibraryPanelService) registerAPIEndpoints() {
if !lps.IsEnabled() {
return
}
lps.RouteRegister.Group("/api/library-panels", func(libraryPanels routing.RouteRegister) {
libraryPanels.Post("/", middleware.ReqSignedIn, binding.Bind(createLibraryPanelCommand{}), routing.Wrap(lps.createHandler))
libraryPanels.Post("/:uid/dashboards/:dashboardId", middleware.ReqSignedIn, routing.Wrap(lps.connectHandler))
libraryPanels.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(lps.deleteHandler))
libraryPanels.Delete("/:uid/dashboards/:dashboardId", middleware.ReqSignedIn, routing.Wrap(lps.disconnectHandler))
libraryPanels.Get("/", middleware.ReqSignedIn, routing.Wrap(lps.getAllHandler))
libraryPanels.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(lps.getHandler))
libraryPanels.Get("/:uid/dashboards/", middleware.ReqSignedIn, routing.Wrap(lps.getConnectedDashboardsHandler))
libraryPanels.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryPanelCommand{}), routing.Wrap(lps.patchHandler))
})
}
// createHandler handles POST /api/library-panels.
func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd createLibraryPanelCommand) response.Response {
panel, err := lps.createLibraryPanel(c, cmd)
if err != nil {
return toLibraryPanelError(err, "Failed to create library panel")
}
return response.JSON(200, util.DynMap{"result": panel})
}
// connectHandler handles POST /api/library-panels/:uid/dashboards/:dashboardId.
func (lps *LibraryPanelService) connectHandler(c *models.ReqContext) response.Response {
err := lps.connectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId"))
if err != nil {
return toLibraryPanelError(err, "Failed to connect library panel")
}
return response.Success("Library panel connected")
}
// deleteHandler handles DELETE /api/library-panels/:uid.
func (lps *LibraryPanelService) deleteHandler(c *models.ReqContext) response.Response {
err := lps.deleteLibraryPanel(c, c.Params(":uid"))
if err != nil {
return toLibraryPanelError(err, "Failed to delete library panel")
}
return response.Success("Library panel deleted")
}
// disconnectHandler handles DELETE /api/library-panels/:uid/dashboards/:dashboardId.
func (lps *LibraryPanelService) disconnectHandler(c *models.ReqContext) response.Response {
err := lps.disconnectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId"))
if err != nil {
return toLibraryPanelError(err, "Failed to disconnect library panel")
}
return response.Success("Library panel disconnected")
}
// getHandler handles GET /api/library-panels/:uid.
func (lps *LibraryPanelService) getHandler(c *models.ReqContext) response.Response {
libraryPanel, err := lps.getLibraryPanel(c, c.Params(":uid"))
if err != nil {
return toLibraryPanelError(err, "Failed to get library panel")
}
return response.JSON(200, util.DynMap{"result": libraryPanel})
}
// getAllHandler handles GET /api/library-panels/.
func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Response {
query := searchLibraryPanelsQuery{
perPage: c.QueryInt("perPage"),
page: c.QueryInt("page"),
searchString: c.Query("searchString"),
sortDirection: c.Query("sortDirection"),
panelFilter: c.Query("panelFilter"),
excludeUID: c.Query("excludeUid"),
folderFilter: c.Query("folderFilter"),
}
libraryPanels, err := lps.getAllLibraryPanels(c, query)
if err != nil {
return toLibraryPanelError(err, "Failed to get library panels")
}
return response.JSON(200, util.DynMap{"result": libraryPanels})
}
// getConnectedDashboardsHandler handles GET /api/library-panels/:uid/dashboards/.
func (lps *LibraryPanelService) getConnectedDashboardsHandler(c *models.ReqContext) response.Response {
dashboardIDs, err := lps.getConnectedDashboards(c, c.Params(":uid"))
if err != nil {
return toLibraryPanelError(err, "Failed to get connected dashboards")
}
return response.JSON(200, util.DynMap{"result": dashboardIDs})
}
// patchHandler handles PATCH /api/library-panels/:uid
func (lps *LibraryPanelService) patchHandler(c *models.ReqContext, cmd patchLibraryPanelCommand) response.Response {
libraryPanel, err := lps.patchLibraryPanel(c, cmd, c.Params(":uid"))
if err != nil {
return toLibraryPanelError(err, "Failed to update library panel")
}
return response.JSON(200, util.DynMap{"result": libraryPanel})
}
func toLibraryPanelError(err error, message string) response.Response {
if errors.Is(err, errLibraryPanelAlreadyExists) {
return response.Error(400, errLibraryPanelAlreadyExists.Error(), err)
}
if errors.Is(err, errLibraryPanelNotFound) {
return response.Error(404, errLibraryPanelNotFound.Error(), err)
}
if errors.Is(err, errLibraryPanelDashboardNotFound) {
return response.Error(404, errLibraryPanelDashboardNotFound.Error(), err)
}
if errors.Is(err, errLibraryPanelVersionMismatch) {
return response.Error(412, errLibraryPanelVersionMismatch.Error(), err)
}
if errors.Is(err, models.ErrFolderNotFound) {
return response.Error(404, models.ErrFolderNotFound.Error(), err)
}
if errors.Is(err, models.ErrFolderAccessDenied) {
return response.Error(403, models.ErrFolderAccessDenied.Error(), err)
}
if errors.Is(err, errLibraryPanelHasConnectedDashboards) {
return response.Error(403, errLibraryPanelHasConnectedDashboards.Error(), err)
}
return response.Error(500, message, err)
}

View File

@ -1,702 +0,0 @@
package librarypanels
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
var (
selectLibrayPanelDTOWithMeta = `
SELECT DISTINCT
lp.name, lp.id, lp.org_id, lp.folder_id, lp.uid, lp.type, lp.description, lp.model, lp.created, lp.created_by, lp.updated, lp.updated_by, lp.version
, 0 AS can_edit
, u1.login AS created_by_name
, u1.email AS created_by_email
, u2.login AS updated_by_name
, u2.email AS updated_by_email
, (SELECT COUNT(dashboard_id) FROM library_panel_dashboard WHERE librarypanel_id = lp.id) AS connected_dashboards
`
fromLibrayPanelDTOWithMeta = `
FROM library_panel AS lp
LEFT JOIN user AS u1 ON lp.created_by = u1.id
LEFT JOIN user AS u2 ON lp.updated_by = u2.id
`
sqlStatmentLibrayPanelDTOWithMeta = selectLibrayPanelDTOWithMeta + fromLibrayPanelDTOWithMeta
)
func syncFieldsWithModel(libraryPanel *LibraryPanel) error {
var model map[string]interface{}
if err := json.Unmarshal(libraryPanel.Model, &model); err != nil {
return err
}
model["title"] = libraryPanel.Name
if model["type"] != nil {
libraryPanel.Type = model["type"].(string)
} else {
model["type"] = libraryPanel.Type
}
if model["description"] != nil {
libraryPanel.Description = model["description"].(string)
} else {
model["description"] = libraryPanel.Description
}
syncedModel, err := json.Marshal(&model)
if err != nil {
return err
}
libraryPanel.Model = syncedModel
return nil
}
// createLibraryPanel adds a Library Panel.
func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd createLibraryPanelCommand) (LibraryPanelDTO, error) {
libraryPanel := LibraryPanel{
OrgID: c.SignedInUser.OrgId,
FolderID: cmd.FolderID,
UID: util.GenerateShortUID(),
Name: cmd.Name,
Model: cmd.Model,
Version: 1,
Created: time.Now(),
Updated: time.Now(),
CreatedBy: c.SignedInUser.UserId,
UpdatedBy: c.SignedInUser.UserId,
}
if err := syncFieldsWithModel(&libraryPanel); err != nil {
return LibraryPanelDTO{}, err
}
err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
if err := lps.requirePermissionsOnFolder(c.SignedInUser, cmd.FolderID); err != nil {
return err
}
if _, err := session.Insert(&libraryPanel); err != nil {
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
return errLibraryPanelAlreadyExists
}
return err
}
return nil
})
dto := LibraryPanelDTO{
ID: libraryPanel.ID,
OrgID: libraryPanel.OrgID,
FolderID: libraryPanel.FolderID,
UID: libraryPanel.UID,
Name: libraryPanel.Name,
Type: libraryPanel.Type,
Description: libraryPanel.Description,
Model: libraryPanel.Model,
Version: libraryPanel.Version,
Meta: LibraryPanelDTOMeta{
CanEdit: true,
ConnectedDashboards: 0,
Created: libraryPanel.Created,
Updated: libraryPanel.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
ID: libraryPanel.CreatedBy,
Name: c.SignedInUser.Login,
AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email),
},
UpdatedBy: LibraryPanelDTOMetaUser{
ID: libraryPanel.UpdatedBy,
Name: c.SignedInUser.Login,
AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email),
},
},
}
return dto, err
}
// connectDashboard adds a connection between a Library Panel and a Dashboard.
func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid string, dashboardID int64) error {
err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
return lps.internalConnectDashboard(session, c.SignedInUser, uid, dashboardID)
})
return err
}
func (lps *LibraryPanelService) internalConnectDashboard(session *sqlstore.DBSession, user *models.SignedInUser,
uid string, dashboardID int64) error {
panel, err := getLibraryPanel(session, uid, user.OrgId)
if err != nil {
return err
}
if err := lps.requirePermissionsOnFolder(user, panel.FolderID); err != nil {
return err
}
libraryPanelDashboard := libraryPanelDashboard{
DashboardID: dashboardID,
LibraryPanelID: panel.ID,
Created: time.Now(),
CreatedBy: user.UserId,
}
if _, err := session.Insert(&libraryPanelDashboard); err != nil {
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
return nil
}
return err
}
return nil
}
// connectLibraryPanelsForDashboard adds connections for all Library Panels in a Dashboard.
func (lps *LibraryPanelService) connectLibraryPanelsForDashboard(c *models.ReqContext, uids []string, dashboardID int64) error {
err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
_, err := session.Exec("DELETE FROM library_panel_dashboard WHERE dashboard_id=?", dashboardID)
if err != nil {
return err
}
for _, uid := range uids {
err := lps.internalConnectDashboard(session, c.SignedInUser, uid, dashboardID)
if err != nil {
return err
}
}
return nil
})
return err
}
// deleteLibraryPanel deletes a Library Panel.
func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid string) error {
return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
if err != nil {
return err
}
if err := lps.requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil {
return err
}
var dashIDs []struct {
DashboardID int64 `xorm:"dashboard_id"`
}
sql := "SELECT dashboard_id FROM library_panel_dashboard WHERE librarypanel_id=?"
if err := session.SQL(sql, panel.ID).Find(&dashIDs); err != nil {
return err
} else if len(dashIDs) > 0 {
return errLibraryPanelHasConnectedDashboards
}
result, err := session.Exec("DELETE FROM library_panel WHERE id=?", panel.ID)
if err != nil {
return err
}
if rowsAffected, err := result.RowsAffected(); err != nil {
return err
} else if rowsAffected != 1 {
return errLibraryPanelNotFound
}
return nil
})
}
// disconnectDashboard deletes a connection between a Library Panel and a Dashboard.
func (lps *LibraryPanelService) disconnectDashboard(c *models.ReqContext, uid string, dashboardID int64) error {
return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
if err != nil {
return err
}
if err := lps.requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil {
return err
}
result, err := session.Exec("DELETE FROM library_panel_dashboard WHERE librarypanel_id=? and dashboard_id=?", panel.ID, dashboardID)
if err != nil {
return err
}
if rowsAffected, err := result.RowsAffected(); err != nil {
return err
} else if rowsAffected != 1 {
return errLibraryPanelDashboardNotFound
}
return nil
})
}
// disconnectLibraryPanelsForDashboard deletes connections for all Library Panels in a Dashboard.
func (lps *LibraryPanelService) disconnectLibraryPanelsForDashboard(c *models.ReqContext, dashboardID int64, panelCount int64) error {
return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
result, err := session.Exec("DELETE FROM library_panel_dashboard WHERE dashboard_id=?", dashboardID)
if err != nil {
return err
}
if rowsAffected, err := result.RowsAffected(); err != nil {
return err
} else if rowsAffected != panelCount {
lps.log.Warn("Number of disconnects does not match number of panels", "dashboard", dashboardID, "rowsAffected", rowsAffected, "panelCount", panelCount)
}
return nil
})
}
// deleteLibraryPanelsInFolder deletes all Library Panels for a folder.
func (lps *LibraryPanelService) deleteLibraryPanelsInFolder(c *models.ReqContext, folderUID string) error {
return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
var folderUIDs []struct {
ID int64 `xorm:"id"`
}
err := session.SQL("SELECT id from dashboard WHERE uid=? AND org_id=? AND is_folder=1", folderUID, c.SignedInUser.OrgId).Find(&folderUIDs)
if err != nil {
return err
}
if len(folderUIDs) != 1 {
return fmt.Errorf("found %d folders, while expecting at most one", len(folderUIDs))
}
folderID := folderUIDs[0].ID
if err := lps.requirePermissionsOnFolder(c.SignedInUser, folderID); err != nil {
return err
}
var dashIDs []struct {
DashboardID int64 `xorm:"dashboard_id"`
}
sql := "SELECT lpd.dashboard_id FROM library_panel AS lp"
sql += " INNER JOIN library_panel_dashboard lpd on lp.id = lpd.librarypanel_id"
sql += " WHERE lp.folder_id=? AND lp.org_id=?"
err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&dashIDs)
if err != nil {
return err
}
if len(dashIDs) > 0 {
return ErrFolderHasConnectedLibraryPanels
}
var panelIDs []struct {
ID int64 `xorm:"id"`
}
err = session.SQL("SELECT id from library_panel WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId).Find(&panelIDs)
if err != nil {
return err
}
for _, panelID := range panelIDs {
_, err := session.Exec("DELETE FROM library_panel_dashboard WHERE librarypanel_id=?", panelID.ID)
if err != nil {
return err
}
}
if _, err := session.Exec("DELETE FROM library_panel WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId); err != nil {
return err
}
return nil
})
}
func getLibraryPanel(session *sqlstore.DBSession, uid string, orgID int64) (LibraryPanelWithMeta, error) {
libraryPanels := make([]LibraryPanelWithMeta, 0)
sql := sqlStatmentLibrayPanelDTOWithMeta + "WHERE lp.uid=? AND lp.org_id=?"
sess := session.SQL(sql, uid, orgID)
err := sess.Find(&libraryPanels)
if err != nil {
return LibraryPanelWithMeta{}, err
}
if len(libraryPanels) == 0 {
return LibraryPanelWithMeta{}, errLibraryPanelNotFound
}
if len(libraryPanels) > 1 {
return LibraryPanelWithMeta{}, fmt.Errorf("found %d panels, while expecting at most one", len(libraryPanels))
}
return libraryPanels[0], nil
}
// getLibraryPanel gets a Library Panel.
func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string) (LibraryPanelDTO, error) {
var libraryPanel LibraryPanelWithMeta
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
libraryPanels := make([]LibraryPanelWithMeta, 0)
builder := sqlstore.SQLBuilder{}
builder.Write(selectLibrayPanelDTOWithMeta)
builder.Write(", 'General' as folder_name ")
builder.Write(", '' as folder_uid ")
builder.Write(fromLibrayPanelDTOWithMeta)
builder.Write(` WHERE lp.uid=? AND lp.org_id=? AND lp.folder_id=0`, uid, c.SignedInUser.OrgId)
builder.Write(" UNION ")
builder.Write(selectLibrayPanelDTOWithMeta)
builder.Write(", dashboard.title as folder_name ")
builder.Write(", dashboard.uid as folder_uid ")
builder.Write(fromLibrayPanelDTOWithMeta)
builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id <> 0")
builder.Write(` WHERE lp.uid=? AND lp.org_id=?`, uid, c.SignedInUser.OrgId)
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
}
builder.Write(` OR dashboard.id=0`)
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
return err
}
if len(libraryPanels) == 0 {
return errLibraryPanelNotFound
}
if len(libraryPanels) > 1 {
return fmt.Errorf("found %d panels, while expecting at most one", len(libraryPanels))
}
libraryPanel = libraryPanels[0]
return nil
})
dto := LibraryPanelDTO{
ID: libraryPanel.ID,
OrgID: libraryPanel.OrgID,
FolderID: libraryPanel.FolderID,
UID: libraryPanel.UID,
Name: libraryPanel.Name,
Type: libraryPanel.Type,
Description: libraryPanel.Description,
Model: libraryPanel.Model,
Version: libraryPanel.Version,
Meta: LibraryPanelDTOMeta{
CanEdit: true,
FolderName: libraryPanel.FolderName,
FolderUID: libraryPanel.FolderUID,
ConnectedDashboards: libraryPanel.ConnectedDashboards,
Created: libraryPanel.Created,
Updated: libraryPanel.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
ID: libraryPanel.CreatedBy,
Name: libraryPanel.CreatedByName,
AvatarUrl: dtos.GetGravatarUrl(libraryPanel.CreatedByEmail),
},
UpdatedBy: LibraryPanelDTOMetaUser{
ID: libraryPanel.UpdatedBy,
Name: libraryPanel.UpdatedByName,
AvatarUrl: dtos.GetGravatarUrl(libraryPanel.UpdatedByEmail),
},
},
}
return dto, err
}
// getAllLibraryPanels gets all library panels.
func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, query searchLibraryPanelsQuery) (LibraryPanelSearchResult, error) {
libraryPanels := make([]LibraryPanelWithMeta, 0)
result := LibraryPanelSearchResult{}
if query.perPage <= 0 {
query.perPage = 100
}
if query.page <= 0 {
query.page = 1
}
var panelFilter []string
if len(strings.TrimSpace(query.panelFilter)) > 0 {
panelFilter = strings.Split(query.panelFilter, ",")
}
folderFilter := parseFolderFilter(query)
if folderFilter.parseError != nil {
return LibraryPanelSearchResult{}, folderFilter.parseError
}
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
builder := sqlstore.SQLBuilder{}
if folderFilter.includeGeneralFolder {
builder.Write(selectLibrayPanelDTOWithMeta)
builder.Write(", 'General' as folder_name ")
builder.Write(", '' as folder_uid ")
builder.Write(fromLibrayPanelDTOWithMeta)
builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId)
writeSearchStringSQL(query, lps.SQLStore, &builder)
writeExcludeSQL(query, &builder)
writePanelFilterSQL(panelFilter, &builder)
builder.Write(" UNION ")
}
builder.Write(selectLibrayPanelDTOWithMeta)
builder.Write(", dashboard.title as folder_name ")
builder.Write(", dashboard.uid as folder_uid ")
builder.Write(fromLibrayPanelDTOWithMeta)
builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id<>0")
builder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId)
writeSearchStringSQL(query, lps.SQLStore, &builder)
writeExcludeSQL(query, &builder)
writePanelFilterSQL(panelFilter, &builder)
if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil {
return err
}
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
}
if query.sortDirection == search.SortAlphaDesc.Name {
builder.Write(" ORDER BY 1 DESC")
} else {
builder.Write(" ORDER BY 1 ASC")
}
writePerPageSQL(query, lps.SQLStore, &builder)
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
return err
}
retDTOs := make([]LibraryPanelDTO, 0)
for _, panel := range libraryPanels {
retDTOs = append(retDTOs, LibraryPanelDTO{
ID: panel.ID,
OrgID: panel.OrgID,
FolderID: panel.FolderID,
UID: panel.UID,
Name: panel.Name,
Type: panel.Type,
Description: panel.Description,
Model: panel.Model,
Version: panel.Version,
Meta: LibraryPanelDTOMeta{
CanEdit: true,
FolderName: panel.FolderName,
FolderUID: panel.FolderUID,
ConnectedDashboards: panel.ConnectedDashboards,
Created: panel.Created,
Updated: panel.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
ID: panel.CreatedBy,
Name: panel.CreatedByName,
AvatarUrl: dtos.GetGravatarUrl(panel.CreatedByEmail),
},
UpdatedBy: LibraryPanelDTOMetaUser{
ID: panel.UpdatedBy,
Name: panel.UpdatedByName,
AvatarUrl: dtos.GetGravatarUrl(panel.UpdatedByEmail),
},
},
})
}
var panels []LibraryPanel
countBuilder := sqlstore.SQLBuilder{}
countBuilder.Write("SELECT * FROM library_panel AS lp")
countBuilder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId)
writeSearchStringSQL(query, lps.SQLStore, &countBuilder)
writeExcludeSQL(query, &countBuilder)
writePanelFilterSQL(panelFilter, &countBuilder)
if err := folderFilter.writeFolderFilterSQL(true, &countBuilder); err != nil {
return err
}
if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&panels); err != nil {
return err
}
result = LibraryPanelSearchResult{
TotalCount: int64(len(panels)),
LibraryPanels: retDTOs,
Page: query.page,
PerPage: query.perPage,
}
return nil
})
return result, err
}
// getConnectedDashboards gets all dashboards connected to a Library Panel.
func (lps *LibraryPanelService) getConnectedDashboards(c *models.ReqContext, uid string) ([]int64, error) {
connectedDashboardIDs := make([]int64, 0)
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
if err != nil {
return err
}
var libraryPanelDashboards []libraryPanelDashboard
builder := sqlstore.SQLBuilder{}
builder.Write("SELECT lpd.* FROM library_panel_dashboard lpd")
builder.Write(" INNER JOIN dashboard AS dashboard on lpd.dashboard_id = dashboard.id")
builder.Write(` WHERE lpd.librarypanel_id=?`, panel.ID)
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
}
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanelDashboards); err != nil {
return err
}
for _, lpd := range libraryPanelDashboards {
connectedDashboardIDs = append(connectedDashboardIDs, lpd.DashboardID)
}
return nil
})
return connectedDashboardIDs, err
}
func (lps *LibraryPanelService) getLibraryPanelsForDashboardID(c *models.ReqContext, dashboardID int64) (map[string]LibraryPanelDTO, error) {
libraryPanelMap := make(map[string]LibraryPanelDTO)
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
var libraryPanels []LibraryPanelWithMeta
sql := selectLibrayPanelDTOWithMeta + ", coalesce(dashboard.title, 'General') AS folder_name, coalesce(dashboard.uid, '') AS folder_uid " + fromLibrayPanelDTOWithMeta + `
LEFT JOIN dashboard AS dashboard ON dashboard.id = lp.folder_id AND dashboard.id=?
INNER JOIN library_panel_dashboard AS lpd ON lpd.librarypanel_id = lp.id AND lpd.dashboard_id=?
`
sess := session.SQL(sql, dashboardID, dashboardID)
err := sess.Find(&libraryPanels)
if err != nil {
return err
}
for _, panel := range libraryPanels {
libraryPanelMap[panel.UID] = LibraryPanelDTO{
ID: panel.ID,
OrgID: panel.OrgID,
FolderID: panel.FolderID,
UID: panel.UID,
Name: panel.Name,
Type: panel.Type,
Description: panel.Description,
Model: panel.Model,
Version: panel.Version,
Meta: LibraryPanelDTOMeta{
CanEdit: panel.CanEdit,
FolderName: panel.FolderName,
FolderUID: panel.FolderUID,
ConnectedDashboards: panel.ConnectedDashboards,
Created: panel.Created,
Updated: panel.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
ID: panel.CreatedBy,
Name: panel.CreatedByName,
AvatarUrl: dtos.GetGravatarUrl(panel.CreatedByEmail),
},
UpdatedBy: LibraryPanelDTOMetaUser{
ID: panel.UpdatedBy,
Name: panel.UpdatedByName,
AvatarUrl: dtos.GetGravatarUrl(panel.UpdatedByEmail),
},
},
}
}
return nil
})
return libraryPanelMap, err
}
func (lps *LibraryPanelService) handleFolderIDPatches(panelToPatch *LibraryPanel, fromFolderID int64,
toFolderID int64, user *models.SignedInUser) error {
// FolderID was not provided in the PATCH request
if toFolderID == -1 {
toFolderID = fromFolderID
}
// FolderID was provided in the PATCH request
if toFolderID != -1 && toFolderID != fromFolderID {
if err := lps.requirePermissionsOnFolder(user, toFolderID); err != nil {
return err
}
}
// Always check permissions for the folder where library panel resides
if err := lps.requirePermissionsOnFolder(user, fromFolderID); err != nil {
return err
}
panelToPatch.FolderID = toFolderID
return nil
}
// patchLibraryPanel updates a Library Panel.
func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patchLibraryPanelCommand, uid string) (LibraryPanelDTO, error) {
var dto LibraryPanelDTO
err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
panelInDB, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
if err != nil {
return err
}
if panelInDB.Version != cmd.Version {
return errLibraryPanelVersionMismatch
}
var libraryPanel = LibraryPanel{
ID: panelInDB.ID,
OrgID: c.SignedInUser.OrgId,
FolderID: cmd.FolderID,
UID: uid,
Name: cmd.Name,
Type: panelInDB.Type,
Description: panelInDB.Description,
Model: cmd.Model,
Version: panelInDB.Version + 1,
Created: panelInDB.Created,
CreatedBy: panelInDB.CreatedBy,
Updated: time.Now(),
UpdatedBy: c.SignedInUser.UserId,
}
if cmd.Name == "" {
libraryPanel.Name = panelInDB.Name
}
if cmd.Model == nil {
libraryPanel.Model = panelInDB.Model
}
if err := lps.handleFolderIDPatches(&libraryPanel, panelInDB.FolderID, cmd.FolderID, c.SignedInUser); err != nil {
return err
}
if err := syncFieldsWithModel(&libraryPanel); err != nil {
return err
}
if rowsAffected, err := session.ID(panelInDB.ID).Update(&libraryPanel); err != nil {
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
return errLibraryPanelAlreadyExists
}
return err
} else if rowsAffected != 1 {
return errLibraryPanelNotFound
}
dto = LibraryPanelDTO{
ID: libraryPanel.ID,
OrgID: libraryPanel.OrgID,
FolderID: libraryPanel.FolderID,
UID: libraryPanel.UID,
Name: libraryPanel.Name,
Type: libraryPanel.Type,
Description: libraryPanel.Description,
Model: libraryPanel.Model,
Version: libraryPanel.Version,
Meta: LibraryPanelDTOMeta{
CanEdit: true,
ConnectedDashboards: panelInDB.ConnectedDashboards,
Created: libraryPanel.Created,
Updated: libraryPanel.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
ID: panelInDB.CreatedBy,
Name: panelInDB.CreatedByName,
AvatarUrl: dtos.GetGravatarUrl(panelInDB.CreatedByEmail),
},
UpdatedBy: LibraryPanelDTOMetaUser{
ID: libraryPanel.UpdatedBy,
Name: c.SignedInUser.Login,
AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email),
},
},
}
return nil
})
return dto, err
}

View File

@ -8,17 +8,18 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting"
)
// LibraryPanelService is the service for the Panel Library feature.
type LibraryPanelService struct {
Cfg *setting.Cfg `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
RouteRegister routing.RouteRegister `inject:""`
log log.Logger
Cfg *setting.Cfg `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
RouteRegister routing.RouteRegister `inject:""`
LibraryElementService *libraryelements.LibraryElementService `inject:""`
log log.Logger
}
func init() {
@ -27,10 +28,7 @@ func init() {
// Init initializes the LibraryPanel service
func (lps *LibraryPanelService) Init() error {
lps.log = log.New("librarypanels")
lps.registerAPIEndpoints()
lps.log = log.New("library-panels")
return nil
}
@ -50,7 +48,7 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte
return nil
}
libraryPanels, err := lps.getLibraryPanelsForDashboardID(c, dash.Id)
elements, err := lps.LibraryElementService.GetElementsForDashboard(c, dash.Id)
if err != nil {
return err
}
@ -69,7 +67,7 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte
return errLibraryPanelHeaderUIDMissing
}
libraryPanelInDB, ok := libraryPanels[uid]
elementInDB, ok := elements[uid]
if !ok {
name := libraryPanel.Get("name").MustString()
elem := dash.Data.Get("panels").GetIndex(i)
@ -83,8 +81,12 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte
continue
}
if libraryelements.LibraryElementKind(elementInDB.Kind) != libraryelements.Panel {
continue
}
// we have a match between what is stored in db and in dashboard json
libraryPanelModel, err := libraryPanelInDB.Model.MarshalJSON()
libraryPanelModel, err := elementInDB.Model.MarshalJSON()
if err != nil {
return fmt.Errorf("could not marshal library panel JSON: %w", err)
}
@ -102,27 +104,26 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte
elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap())
elem.Set("id", panelAsJSON.Get("id").MustInt64())
elem.Set("libraryPanel", map[string]interface{}{
"uid": libraryPanelInDB.UID,
"name": libraryPanelInDB.Name,
"type": libraryPanelInDB.Type,
"description": libraryPanelInDB.Description,
"version": libraryPanelInDB.Version,
"uid": elementInDB.UID,
"name": elementInDB.Name,
"type": elementInDB.Type,
"description": elementInDB.Description,
"version": elementInDB.Version,
"meta": map[string]interface{}{
"canEdit": libraryPanelInDB.Meta.CanEdit,
"folderName": libraryPanelInDB.Meta.FolderName,
"folderUid": libraryPanelInDB.Meta.FolderUID,
"connectedDashboards": libraryPanelInDB.Meta.ConnectedDashboards,
"created": libraryPanelInDB.Meta.Created,
"updated": libraryPanelInDB.Meta.Updated,
"folderName": elementInDB.Meta.FolderName,
"folderUid": elementInDB.Meta.FolderUID,
"connectedDashboards": elementInDB.Meta.Connections,
"created": elementInDB.Meta.Created,
"updated": elementInDB.Meta.Updated,
"createdBy": map[string]interface{}{
"id": libraryPanelInDB.Meta.CreatedBy.ID,
"name": libraryPanelInDB.Meta.CreatedBy.Name,
"avatarUrl": libraryPanelInDB.Meta.CreatedBy.AvatarUrl,
"id": elementInDB.Meta.CreatedBy.ID,
"name": elementInDB.Meta.CreatedBy.Name,
"avatarUrl": elementInDB.Meta.CreatedBy.AvatarURL,
},
"updatedBy": map[string]interface{}{
"id": libraryPanelInDB.Meta.UpdatedBy.ID,
"name": libraryPanelInDB.Meta.UpdatedBy.Name,
"avatarUrl": libraryPanelInDB.Meta.UpdatedBy.AvatarUrl,
"id": elementInDB.Meta.UpdatedBy.ID,
"name": elementInDB.Meta.UpdatedBy.Name,
"avatarUrl": elementInDB.Meta.UpdatedBy.AvatarURL,
},
},
})
@ -195,88 +196,5 @@ func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqCo
libraryPanels = append(libraryPanels, uid)
}
return lps.connectLibraryPanelsForDashboard(c, libraryPanels, dash.Id)
}
// DisconnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and disconnects any library panels from the dashboard.
func (lps *LibraryPanelService) DisconnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error {
if !lps.IsEnabled() {
return nil
}
panels := dash.Data.Get("panels").MustArray()
panelCount := int64(0)
for _, panel := range panels {
panelAsJSON := simplejson.NewFromAny(panel)
libraryPanel := panelAsJSON.Get("libraryPanel")
if libraryPanel.Interface() == nil {
continue
}
// we have a library panel
uid := libraryPanel.Get("uid").MustString()
if len(uid) == 0 {
return errLibraryPanelHeaderUIDMissing
}
panelCount++
}
return lps.disconnectLibraryPanelsForDashboard(c, dash.Id, panelCount)
}
func (lps *LibraryPanelService) DeleteLibraryPanelsInFolder(c *models.ReqContext, folderUID string) error {
if !lps.IsEnabled() {
return nil
}
return lps.deleteLibraryPanelsInFolder(c, folderUID)
}
// AddMigration defines database migrations.
// If Panel Library is not enabled does nothing.
func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) {
if !lps.IsEnabled() {
return
}
libraryPanelV1 := migrator.Table{
Name: "library_panel",
Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "folder_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
{Name: "name", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
{Name: "type", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
{Name: "description", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
{Name: "model", Type: migrator.DB_Text, Nullable: false},
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
{Name: "created_by", Type: migrator.DB_BigInt, Nullable: false},
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
{Name: "updated_by", Type: migrator.DB_BigInt, Nullable: false},
{Name: "version", Type: migrator.DB_BigInt, Nullable: false},
},
Indices: []*migrator.Index{
{Cols: []string{"org_id", "folder_id", "name"}, Type: migrator.UniqueIndex},
},
}
mg.AddMigration("create library_panel table v1", migrator.NewAddTableMigration(libraryPanelV1))
mg.AddMigration("add index library_panel org_id & folder_id & name", migrator.NewAddIndexMigration(libraryPanelV1, libraryPanelV1.Indices[0]))
libraryPanelDashboardV1 := migrator.Table{
Name: "library_panel_dashboard",
Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "librarypanel_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "dashboard_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
{Name: "created_by", Type: migrator.DB_BigInt, Nullable: false},
},
Indices: []*migrator.Index{
{Cols: []string{"librarypanel_id", "dashboard_id"}, Type: migrator.UniqueIndex},
},
}
mg.AddMigration("create library_panel_dashboard table v1", migrator.NewAddTableMigration(libraryPanelDashboardV1))
mg.AddMigration("add index library_panel_dashboard librarypanel_id & dashboard_id", migrator.NewAddIndexMigration(libraryPanelDashboardV1, libraryPanelDashboardV1.Indices[0]))
return lps.LibraryElementService.ConnectElementsToDashboard(c, libraryPanels, dash.Id)
}

View File

@ -1,97 +0,0 @@
package librarypanels
import (
"encoding/json"
"strconv"
"testing"
"github.com/stretchr/testify/require"
)
func TestConnectLibraryPanel(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to create a connection for a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to create a connection that already exists, it should succeed",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
})
}
func TestDisconnectLibraryPanel(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to remove a connection with a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", ":dashboardId": "1"})
resp := sc.service.disconnectHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to remove a connection that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.disconnectHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to remove a connection that does exist, it should succeed",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
resp = sc.service.disconnectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
})
}
func TestGetConnectedDashboards(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to get connected dashboards for a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
resp := sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to get connected dashboards for a library panel that exists, but has no connections, it should return none",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var dashResult libraryPanelDashboardsResult
err := json.Unmarshal(resp.Body(), &dashResult)
require.NoError(t, err)
require.Equal(t, 0, len(dashResult.Result))
})
scenarioWithLibraryPanel(t, "When an admin tries to get connected dashboards for a library panel that exists and has connections, it should return connected dashboard IDs",
func(t *testing.T, sc scenarioContext) {
firstDash := createDashboard(t, sc.sqlStore, sc.user, "Dash 1", 0)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": strconv.FormatInt(firstDash.Id, 10)})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
secondDash := createDashboard(t, sc.sqlStore, sc.user, "Dash 2", 0)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": strconv.FormatInt(secondDash.Id, 10)})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp = sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var dashResult libraryPanelDashboardsResult
err := json.Unmarshal(resp.Body(), &dashResult)
require.NoError(t, err)
require.Equal(t, 2, len(dashResult.Result))
require.Equal(t, firstDash.Id, dashResult.Result[0])
require.Equal(t, secondDash.Id, dashResult.Result[1])
})
}

View File

@ -1,44 +0,0 @@
package librarypanels
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
)
func TestDeleteLibraryPanel(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
resp := sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that exists, it should succeed",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel in another org, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
sc.reqContext.SignedInUser.OrgId = 2
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
resp := sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that is connected, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp = sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 403, resp.Status())
})
}

View File

@ -1,91 +0,0 @@
package librarypanels
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
)
func TestGetLibraryPanel(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
resp := sc.service.getHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that exists, it should succeed and return correct result",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getHandler(sc.reqContext)
var result = validateAndUnMarshalResponse(t, resp)
var expected = libraryPanelResult{
Result: libraryPanel{
ID: 1,
OrgID: 1,
FolderID: 1,
UID: result.Result.UID,
Name: "Text - Library Panel",
Type: "text",
Description: "A description",
Model: map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}",
"description": "A description",
"id": float64(1),
"title": "Text - Library Panel",
"type": "text",
},
Version: 1,
Meta: LibraryPanelDTOMeta{
CanEdit: true,
FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid,
ConnectedDashboards: 0,
Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
ID: 1,
Name: UserInDbName,
AvatarUrl: UserInDbAvatar,
},
UpdatedBy: LibraryPanelDTOMetaUser{
ID: 1,
Name: UserInDbName,
AvatarUrl: UserInDbAvatar,
},
},
},
}
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that exists in an other org, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
sc.reqContext.SignedInUser.OrgId = 2
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
resp := sc.service.getHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
scenarioWithLibraryPanel(t, "When an admin tries to get a library panel with 2 connected dashboards, it should succeed and return correct connected dashboards",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "2"})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp = sc.service.getHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var result = validateAndUnMarshalResponse(t, resp)
require.Equal(t, int64(2), result.Result.Meta.ConnectedDashboards)
})
}

View File

@ -9,15 +9,16 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
dboards "github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
@ -28,10 +29,6 @@ const UserInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad"
func TestLoadLibraryPanelsForDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel, it should copy JSON properties from library panel",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
@ -59,13 +56,19 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) {
},
}
dash := models.Dashboard{
Id: 1,
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing LoadLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.elementService.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
err := sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, &dash)
err = sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, dashInDB)
require.NoError(t, err)
expectedJSON := map[string]interface{}{
"title": "Testing LoadLibraryPanelsForDashboard",
"uid": dashInDB.Uid,
"version": dashInDB.Version,
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
@ -93,7 +96,6 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) {
"description": sc.initialResult.Result.Description,
"version": sc.initialResult.Result.Version,
"meta": map[string]interface{}{
"canEdit": false,
"folderName": "ScenarioFolder",
"folderUid": sc.folder.Uid,
"connectedDashboards": int64(1),
@ -124,10 +126,6 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel without uid, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
@ -154,11 +152,14 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) {
},
}
dash := models.Dashboard{
Id: 1,
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing LoadLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.elementService.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
err := sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, &dash)
err = sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, dashInDB)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
})
@ -191,13 +192,17 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) {
},
}
dash := models.Dashboard{
Id: 1,
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing LoadLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, &dash)
err := sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, dashInDB)
require.NoError(t, err)
expectedJSON := map[string]interface{}{
"title": "Testing LoadLibraryPanelsForDashboard",
"uid": dashInDB.Uid,
"version": dashInDB.Version,
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
@ -264,13 +269,17 @@ func TestCleanLibraryPanelsForDashboard(t *testing.T) {
},
}
dash := models.Dashboard{
Id: 1,
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing CleanLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.CleanLibraryPanelsForDashboard(&dash)
err := sc.service.CleanLibraryPanelsForDashboard(dashInDB)
require.NoError(t, err)
expectedJSON := map[string]interface{}{
"title": "Testing CleanLibraryPanelsForDashboard",
"uid": dashInDB.Uid,
"version": dashInDB.Version,
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
@ -333,11 +342,12 @@ func TestCleanLibraryPanelsForDashboard(t *testing.T) {
},
}
dash := models.Dashboard{
Id: 1,
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing CleanLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.CleanLibraryPanelsForDashboard(&dash)
err := sc.service.CleanLibraryPanelsForDashboard(dashInDB)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
})
@ -372,11 +382,12 @@ func TestCleanLibraryPanelsForDashboard(t *testing.T) {
},
}
dash := models.Dashboard{
Id: 1,
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing CleanLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.CleanLibraryPanelsForDashboard(&dash)
err := sc.service.CleanLibraryPanelsForDashboard(dashInDB)
require.EqualError(t, err, errLibraryPanelHeaderNameMissing.Error())
})
}
@ -414,22 +425,18 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
},
}
dash := models.Dashboard{
Id: int64(1),
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash)
err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, dashInDB)
require.NoError(t, err)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var dashResult libraryPanelDashboardsResult
err = json.Unmarshal(resp.Body(), &dashResult)
elements, err := sc.elementService.GetElementsForDashboard(sc.reqContext, dashInDB.Id)
require.NoError(t, err)
require.Len(t, dashResult.Result, 1)
require.Equal(t, int64(1), dashResult.Result[0])
require.Len(t, elements, 1)
require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID)
})
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail",
@ -463,23 +470,32 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
},
}
dash := models.Dashboard{
Id: int64(1),
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash)
err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, dashInDB)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
})
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with unused/removed library panels, it should disconnect unused/removed library panels",
func(t *testing.T, sc scenarioContext) {
command := getCreateCommand(sc.folder.Id, "Unused Libray Panel")
resp := sc.service.createHandler(sc.reqContext, command)
var unused = validateAndUnMarshalResponse(t, resp)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": unused.Result.UID, ":dashboardId": "1"})
resp = sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
unused, err := sc.elementService.CreateElement(sc.reqContext, libraryelements.CreateLibraryElementCommand{
FolderID: sc.folder.Id,
Name: "Unused Libray Panel",
Model: []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 4,
"title": "Unused Libray Panel",
"type": "text",
"description": "Unused description"
}
`),
Kind: int64(libraryelements.Panel),
})
require.NoError(t, err)
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
@ -492,7 +508,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
},
},
map[string]interface{}{
"id": int64(2),
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
@ -501,258 +517,124 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
"uid": unused.UID,
"name": unused.Name,
},
"title": "Text - Library Panel",
"type": "text",
"title": "Unused Libray Panel",
"description": "Unused description",
},
},
}
dash := models.Dashboard{
Id: int64(1),
Data: simplejson.NewFromAny(dashJSON),
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
err := sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash)
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err = sc.elementService.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp = sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var existingResult libraryPanelDashboardsResult
err = json.Unmarshal(resp.Body(), &existingResult)
require.NoError(t, err)
require.Len(t, existingResult.Result, 1)
require.Equal(t, int64(1), existingResult.Result[0])
sc.reqContext.ReplaceAllParams(map[string]string{":uid": unused.Result.UID})
resp = sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var unusedResult libraryPanelDashboardsResult
err = json.Unmarshal(resp.Body(), &unusedResult)
require.NoError(t, err)
require.Len(t, unusedResult.Result, 0)
})
}
func TestDisconnectLibraryPanelsForDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to delete a dashboard with a library panel, it should disconnect the two",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
"title": "Text - Library Panel",
"type": "text",
panelJSON := []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
}
dash := models.Dashboard{
Id: int64(1),
Data: simplejson.NewFromAny(dashJSON),
}
err := sc.service.DisconnectLibraryPanelsForDashboard(sc.reqContext, &dash)
require.NoError(t, err)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp = sc.service.getConnectedDashboardsHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var dashResult libraryPanelDashboardsResult
err = json.Unmarshal(resp.Body(), &dashResult)
require.NoError(t, err)
require.Empty(t, dashResult.Result)
})
scenarioWithLibraryPanel(t, "When an admin tries to delete a dashboard with a library panel without uid, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"name": sc.initialResult.Result.Name,
},
"title": "Text - Library Panel",
"type": "text",
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
"title": "Text - Library Panel",
"type": "text",
},
}
dash := models.Dashboard{
Id: int64(1),
Data: simplejson.NewFromAny(dashJSON),
}
err := sc.service.DisconnectLibraryPanelsForDashboard(sc.reqContext, &dash)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
})
}
func TestDeleteLibraryPanelsInFolder(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to delete a folder that contains connected library panels, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
resp := sc.service.connectHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
err := sc.service.DeleteLibraryPanelsInFolder(sc.reqContext, sc.folder.Uid)
require.EqualError(t, err, ErrFolderHasConnectedLibraryPanels.Error())
})
scenarioWithLibraryPanel(t, "When an admin tries to delete a folder that contains disconnected library panels, it should delete all disconnected library panels too",
func(t *testing.T, sc scenarioContext) {
resp := sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var result libraryPanelsSearch
err := json.Unmarshal(resp.Body(), &result)
dashInDB.Data.Set("panels", panelJSON)
err = sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, dashInDB)
require.NoError(t, err)
require.NotNil(t, result.Result)
require.Equal(t, 1, len(result.Result.LibraryPanels))
err = sc.service.DeleteLibraryPanelsInFolder(sc.reqContext, sc.folder.Uid)
elements, err := sc.elementService.GetElementsForDashboard(sc.reqContext, dashInDB.Id)
require.NoError(t, err)
resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
err = json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
require.NotNil(t, result.Result)
require.Equal(t, 0, len(result.Result.LibraryPanels))
require.Len(t, elements, 1)
require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID)
})
}
type libraryPanel struct {
ID int64 `json:"id"`
OrgID int64 `json:"orgId"`
FolderID int64 `json:"folderId"`
UID string `json:"uid"`
Name string `json:"name"`
ID int64
OrgID int64
FolderID int64
UID string
Name string
Type string
Description string
Model map[string]interface{} `json:"model"`
Version int64 `json:"version"`
Meta LibraryPanelDTOMeta `json:"meta"`
Model map[string]interface{}
Version int64
Meta libraryelements.LibraryElementDTOMeta
}
type libraryPanelResult struct {
Result libraryPanel `json:"result"`
}
type libraryPanelsSearch struct {
Result libraryPanelsSearchResult `json:"result"`
}
type libraryPanelsSearchResult struct {
TotalCount int64 `json:"totalCount"`
LibraryPanels []libraryPanel `json:"libraryPanels"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
type libraryPanelDashboardsResult struct {
Result []int64 `json:"result"`
}
func overrideLibraryPanelServiceInRegistry(cfg *setting.Cfg) LibraryPanelService {
lps := LibraryPanelService{
func overrideLibraryServicesInRegistry(cfg *setting.Cfg) (*LibraryPanelService, *libraryelements.LibraryElementService) {
les := libraryelements.LibraryElementService{
SQLStore: nil,
Cfg: cfg,
}
overrideServiceFunc := func(d registry.Descriptor) (*registry.Descriptor, bool) {
elementsOverride := func(d registry.Descriptor) (*registry.Descriptor, bool) {
descriptor := registry.Descriptor{
Name: "LibraryPanelService",
Instance: &lps,
InitPriority: 0,
Name: "LibraryElementService",
Instance: &les,
}
return &descriptor, true
}
registry.RegisterOverride(overrideServiceFunc)
registry.RegisterOverride(elementsOverride)
return lps
}
func getCreateCommand(folderID int64, name string) createLibraryPanelCommand {
command := getCreateCommandWithModel(folderID, name, []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`))
return command
}
func getCreateCommandWithModel(folderID int64, name string, model []byte) createLibraryPanelCommand {
command := createLibraryPanelCommand{
FolderID: folderID,
Name: name,
Model: model,
lps := LibraryPanelService{
SQLStore: nil,
Cfg: cfg,
LibraryElementService: &les,
}
return command
panelsOverride := func(d registry.Descriptor) (*registry.Descriptor, bool) {
descriptor := registry.Descriptor{
Name: "LibraryPanelService",
Instance: &lps,
}
return &descriptor, true
}
registry.RegisterOverride(panelsOverride)
return &lps, &les
}
type scenarioContext struct {
ctx *macaron.Context
service *LibraryPanelService
reqContext *models.ReqContext
user models.SignedInUser
folder *models.Folder
initialResult libraryPanelResult
sqlStore *sqlstore.SQLStore
ctx *macaron.Context
service *LibraryPanelService
elementService *libraryelements.LibraryElementService
reqContext *models.ReqContext
user models.SignedInUser
folder *models.Folder
initialResult libraryPanelResult
sqlStore *sqlstore.SQLStore
}
type folderACLItem struct {
@ -760,9 +642,7 @@ type folderACLItem struct {
permission models.PermissionType
}
func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, title string,
folderID int64) *models.Dashboard {
dash := models.NewDashboard(title)
func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard {
dash.FolderId = folderID
dashItem := &dashboards.SaveDashboardDTO{
Dashboard: dash,
@ -824,25 +704,44 @@ func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64,
require.NoError(t, err)
}
func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryPanelResult {
t.Helper()
require.Equal(t, 200, resp.Status())
var result = libraryPanelResult{}
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
return result
}
func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
testScenario(t, desc, func(t *testing.T, sc scenarioContext) {
command := getCreateCommand(sc.folder.Id, "Text - Library Panel")
resp := sc.service.createHandler(sc.reqContext, command)
sc.initialResult = validateAndUnMarshalResponse(t, resp)
command := libraryelements.CreateLibraryElementCommand{
FolderID: sc.folder.Id,
Name: "Text - Library Panel",
Model: []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`),
Kind: int64(libraryelements.Panel),
}
resp, err := sc.elementService.CreateElement(sc.reqContext, command)
require.NoError(t, err)
var model map[string]interface{}
err = json.Unmarshal(resp.Model, &model)
require.NoError(t, err)
sc.initialResult = libraryPanelResult{
Result: libraryPanel{
ID: resp.ID,
OrgID: resp.OrgID,
FolderID: resp.FolderID,
UID: resp.UID,
Name: resp.Name,
Type: resp.Type,
Description: resp.Description,
Model: model,
Version: resp.Version,
Meta: resp.Meta,
},
}
fn(t, sc)
})
@ -867,10 +766,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
cfg.FeatureToggles = map[string]bool{"panelLibrary": true}
// Because the LibraryPanelService is behind a feature toggle, we need to override the service in the registry
// with a Cfg that contains the feature toggle so migrations are run properly
service := overrideLibraryPanelServiceInRegistry(cfg)
service, elementService := overrideLibraryServicesInRegistry(cfg)
// We need to assign SQLStore after the override and migrations are done
sqlStore := sqlstore.InitTestDB(t)
elementService.SQLStore = sqlStore
service.SQLStore = sqlStore
user := models.SignedInUser{
@ -895,10 +795,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
require.NoError(t, err)
sc := scenarioContext{
user: user,
ctx: &ctx,
service: &service,
sqlStore: sqlStore,
user: user,
ctx: &ctx,
service: service,
elementService: elementService,
sqlStore: sqlStore,
reqContext: &models.ReqContext{
Context: &ctx,
SignedInUser: &user,

View File

@ -1,154 +1,12 @@
package librarypanels
import (
"encoding/json"
"errors"
"time"
)
// LibraryPanel is the model for library panel definitions.
type LibraryPanel struct {
ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
FolderID int64 `xorm:"folder_id"`
UID string `xorm:"uid"`
Name string
Type string
Description string
Model json.RawMessage
Version int64
Created time.Time
Updated time.Time
CreatedBy int64
UpdatedBy int64
}
// LibraryPanelWithMeta is the model used to retrieve library panels with additional meta information.
type LibraryPanelWithMeta struct {
ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
FolderID int64 `xorm:"folder_id"`
UID string `xorm:"uid"`
Name string
Type string
Description string
Model json.RawMessage
Version int64
Created time.Time
Updated time.Time
CanEdit bool
FolderName string
FolderUID string `xorm:"folder_uid"`
ConnectedDashboards int64
CreatedBy int64
UpdatedBy int64
CreatedByName string
CreatedByEmail string
UpdatedByName string
UpdatedByEmail string
}
// LibraryPanelDTO is the frontend DTO for library panels.
type LibraryPanelDTO struct {
ID int64 `json:"id"`
OrgID int64 `json:"orgId"`
FolderID int64 `json:"folderId"`
UID string `json:"uid"`
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Model json.RawMessage `json:"model"`
Version int64 `json:"version"`
Meta LibraryPanelDTOMeta `json:"meta"`
}
// LibraryPanelSearchResult is the search result for library panels.
type LibraryPanelSearchResult struct {
TotalCount int64 `json:"totalCount"`
LibraryPanels []LibraryPanelDTO `json:"libraryPanels"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
// LibraryPanelDTOMeta is the meta information for LibraryPanelDTO.
type LibraryPanelDTOMeta struct {
CanEdit bool `json:"canEdit"`
FolderName string `json:"folderName"`
FolderUID string `json:"folderUid"`
ConnectedDashboards int64 `json:"connectedDashboards"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
CreatedBy LibraryPanelDTOMetaUser `json:"createdBy"`
UpdatedBy LibraryPanelDTOMetaUser `json:"updatedBy"`
}
// LibraryPanelDTOMetaUser is the meta information for user that creates/changes the library panel.
type LibraryPanelDTOMetaUser struct {
ID int64 `json:"id"`
Name string `json:"name"`
AvatarUrl string `json:"avatarUrl"`
}
// libraryPanelDashboard is the model for library panel connections.
type libraryPanelDashboard struct {
ID int64 `xorm:"pk autoincr 'id'"`
LibraryPanelID int64 `xorm:"librarypanel_id"`
DashboardID int64 `xorm:"dashboard_id"`
Created time.Time
CreatedBy int64
}
var (
// errLibraryPanelAlreadyExists is an error for when the user tries to add a library panel that already exists.
errLibraryPanelAlreadyExists = errors.New("library panel with that name already exists")
// errLibraryPanelNotFound is an error for when a library panel can't be found.
errLibraryPanelNotFound = errors.New("library panel could not be found")
// errLibraryPanelDashboardNotFound is an error for when a library panel connection can't be found.
errLibraryPanelDashboardNotFound = errors.New("library panel connection could not be found")
// errLibraryPanelHeaderUIDMissing is an error for when a library panel header is missing the uid property.
errLibraryPanelHeaderUIDMissing = errors.New("library panel header is missing required property uid")
// errLibraryPanelHeaderNameMissing is an error for when a library panel header is missing the name property.
errLibraryPanelHeaderNameMissing = errors.New("library panel header is missing required property name")
// ErrFolderHasConnectedLibraryPanels is an error for when an user deletes a folder that contains connected library panels.
ErrFolderHasConnectedLibraryPanels = errors.New("folder contains library panels that are linked to dashboards")
// errLibraryPanelVersionMismatch is an error for when a library panel has been changed by someone else.
errLibraryPanelVersionMismatch = errors.New("the library panel has been changed by someone else")
// errLibraryPanelHasConnectedDashboards is an error for when an user deletes a library panel that is connected to library panels.
errLibraryPanelHasConnectedDashboards = errors.New("the library panel is linked to dashboards")
)
// Commands
// createLibraryPanelCommand is the command for adding a LibraryPanel
type createLibraryPanelCommand struct {
FolderID int64 `json:"folderId"`
Name string `json:"name"`
Model json.RawMessage `json:"model"`
}
// patchLibraryPanelCommand is the command for patching a LibraryPanel
type patchLibraryPanelCommand struct {
FolderID int64 `json:"folderId" binding:"Default(-1)"`
Name string `json:"name"`
Model json.RawMessage `json:"model"`
Version int64 `json:"version" binding:"Required"`
}
// searchLibraryPanelsQuery is the query used for searching for LibraryPanels
type searchLibraryPanelsQuery struct {
perPage int
page int
searchString string
sortDirection string
panelFilter string
excludeUID string
folderFilter string
}

View File

@ -12,7 +12,7 @@ import store from 'app/core/store';
import { addPanel } from 'app/features/dashboard/state/reducers';
import { DashboardModel, PanelModel } from '../../state';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { LibraryPanelDTO } from '../../../library-panels/types';
import { LibraryElementDTO } from '../../../library-panels/types';
import { toPanelModelLibraryPanel } from '../../../library-panels/utils';
import {
LibraryPanelsSearch,
@ -108,7 +108,7 @@ export const AddPanelWidgetUnconnected: React.FC<Props> = ({ panel, dashboard })
dashboard.removePanel(panel);
};
const onAddLibraryPanel = (panelInfo: LibraryPanelDTO) => {
const onAddLibraryPanel = (panelInfo: LibraryElementDTO) => {
const { gridPos } = panel;
const newPanel: PanelModel = {

View File

@ -6,7 +6,7 @@ import { GrafanaRouteComponentProps } from '../../core/navigation/types';
import { StoreState } from '../../types';
import { getNavModel } from '../../core/selectors/navModel';
import { getLoadingNav } from './state/navModel';
import { LibraryPanelDTO } from '../library-panels/types';
import { LibraryElementDTO } from '../library-panels/types';
import Page from '../../core/components/Page/Page';
import { LibraryPanelsSearch } from '../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
import { OpenLibraryPanelModal } from '../library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal';
@ -33,7 +33,7 @@ export type Props = OwnProps & ConnectedProps<typeof connector>;
export function FolderLibraryPanelsPage({ navModel, getFolderByUid, folderUid, folder }: Props): JSX.Element {
const { loading } = useAsync<void>(async () => await getFolderByUid(folderUid), [getFolderByUid, folderUid]);
const [selected, setSelected] = useState<LibraryPanelDTO | undefined>(undefined);
const [selected, setSelected] = useState<LibraryElementDTO | undefined>(undefined);
return (
<Page navModel={navModel}>

View File

@ -22,6 +22,16 @@ export function buildNavModel(folder: FolderDTO): NavModelItem {
],
};
if (getConfig().featureToggles.panelLibrary) {
model.children.push({
active: false,
icon: 'library-panel',
id: `folder-library-panels-${folder.uid}`,
text: 'Panels',
url: `${folder.url}/library-panels`,
});
}
if (folder.canAdmin) {
model.children.push({
active: false,
@ -42,16 +52,6 @@ export function buildNavModel(folder: FolderDTO): NavModelItem {
});
}
if (getConfig().featureToggles.panelLibrary) {
model.children.push({
active: false,
icon: 'library-panel',
id: `folder-library-panels-${folder.uid}`,
text: 'Panels',
url: `${folder.url}/library-panels`,
});
}
return model;
}

View File

@ -6,7 +6,7 @@ import { StoreState } from '../../types';
import { getNavModel } from '../../core/selectors/navModel';
import Page from '../../core/components/Page/Page';
import { LibraryPanelsSearch } from './components/LibraryPanelsSearch/LibraryPanelsSearch';
import { LibraryPanelDTO } from './types';
import { LibraryElementDTO } from './types';
import { OpenLibraryPanelModal } from './components/OpenLibraryPanelModal/OpenLibraryPanelModal';
const mapStateToProps = (state: StoreState) => ({
@ -20,7 +20,7 @@ interface OwnProps extends GrafanaRouteComponentProps {}
type Props = OwnProps & ConnectedProps<typeof connector>;
export const LibraryPanelsPage: FC<Props> = ({ navModel }) => {
const [selected, setSelected] = useState<LibraryPanelDTO | undefined>(undefined);
const [selected, setSelected] = useState<LibraryElementDTO | undefined>(undefined);
return (
<Page navModel={navModel}>

View File

@ -2,14 +2,14 @@ import React, { FC, useEffect, useMemo, useReducer } from 'react';
import { Button, Modal, useStyles } from '@grafana/ui';
import { LoadingState } from '@grafana/data';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { asyncDispatcher } from '../LibraryPanelsView/actions';
import { deleteLibraryPanelModalReducer, initialDeleteLibraryPanelModalState } from './reducer';
import { getConnectedDashboards } from './actions';
import { getModalStyles } from '../../styles';
interface Props {
libraryPanel: LibraryPanelDTO;
libraryPanel: LibraryElementDTO;
onConfirm: () => void;
onDismiss: () => void;
}

View File

@ -1,8 +1,8 @@
import { DispatchResult, LibraryPanelDTO } from '../../types';
import { DispatchResult, LibraryElementDTO } from '../../types';
import { getConnectedDashboards as apiGetConnectedDashboards } from '../../state/api';
import { searchCompleted } from './reducer';
export function getConnectedDashboards(libraryPanel: LibraryPanelDTO): DispatchResult {
export function getConnectedDashboards(libraryPanel: LibraryElementDTO): DispatchResult {
return async function (dispatch) {
const dashboards = await apiGetConnectedDashboards(libraryPanel.uid);
dispatch(searchCompleted({ dashboards }));

View File

@ -4,14 +4,14 @@ import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Icon, Link, useStyles2 } from '@grafana/ui';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { PanelTypeCard } from 'app/features/dashboard/components/VizTypePicker/PanelTypeCard';
import { DeleteLibraryPanelModal } from '../DeleteLibraryPanelModal/DeleteLibraryPanelModal';
export interface LibraryPanelCardProps {
libraryPanel: LibraryPanelDTO;
onClick: (panel: LibraryPanelDTO) => void;
onDelete?: (panel: LibraryPanelDTO) => void;
libraryPanel: LibraryElementDTO;
onClick: (panel: LibraryElementDTO) => void;
onDelete?: (panel: LibraryElementDTO) => void;
showSecondaryActions?: boolean;
}
@ -54,7 +54,7 @@ export const LibraryPanelCard: React.FC<LibraryPanelCardProps & { children?: JSX
};
interface FolderLinkProps {
libraryPanel: LibraryPanelDTO;
libraryPanel: LibraryElementDTO;
}
function FolderLink({ libraryPanel }: FolderLinkProps): JSX.Element {
@ -64,18 +64,18 @@ function FolderLink({ libraryPanel }: FolderLinkProps): JSX.Element {
return (
<span className={styles.metaContainer}>
<Icon name={'folder'} size="sm" />
{libraryPanel.meta.folderName}
<span>{libraryPanel.meta.folderName}</span>
</span>
);
}
return (
<Link href={`/dashboards/f/${libraryPanel.meta.folderUid}`}>
<span className={styles.metaContainer}>
<span className={styles.metaContainer}>
<Link href={`/dashboards/f/${libraryPanel.meta.folderUid}`}>
<Icon name={'folder-upload'} size="sm" />
{libraryPanel.meta.folderName}
</span>
</Link>
<span>{libraryPanel.meta.folderName}</span>
</Link>
</span>
);
}

View File

@ -6,7 +6,7 @@ import { PanelPluginMeta, PluginType } from '@grafana/data';
import { LibraryPanelsSearch, LibraryPanelsSearchProps } from './LibraryPanelsSearch';
import * as api from '../../state/api';
import { LibraryPanelSearchResult } from '../../types';
import { LibraryElementKind, LibraryElementsSearchResult } from '../../types';
import { backendSrv } from '../../../../core/services/backend_srv';
import * as viztypepicker from '../../../dashboard/components/VizTypePicker/VizTypePicker';
@ -38,7 +38,7 @@ jest.mock('debounce-promise', () => {
async function getTestContext(
propOverrides: Partial<LibraryPanelsSearchProps> = {},
searchResult: LibraryPanelSearchResult = { libraryPanels: [], perPage: 40, page: 1, totalCount: 0 }
searchResult: LibraryElementsSearchResult = { elements: [], perPage: 40, page: 1, totalCount: 0 }
) {
jest.clearAllMocks();
const pluginInfo: any = { logos: { small: '', large: '' } };
@ -102,7 +102,7 @@ describe('LibraryPanelsSearch', () => {
searchString: 'a',
folderFilter: [],
page: 0,
panelFilter: [],
typeFilter: [],
perPage: 40,
});
});
@ -130,7 +130,7 @@ describe('LibraryPanelsSearch', () => {
sortDirection: 'alpha-desc',
folderFilter: [],
page: 0,
panelFilter: [],
typeFilter: [],
perPage: 40,
});
});
@ -158,7 +158,7 @@ describe('LibraryPanelsSearch', () => {
searchString: '',
folderFilter: [],
page: 0,
panelFilter: ['graph', 'timeseries'],
typeFilter: ['graph', 'timeseries'],
perPage: 40,
});
});
@ -188,7 +188,7 @@ describe('LibraryPanelsSearch', () => {
searchString: '',
folderFilter: ['0'],
page: 0,
panelFilter: [],
typeFilter: [],
perPage: 40,
});
});
@ -203,10 +203,11 @@ describe('LibraryPanelsSearch', () => {
page: 1,
totalCount: 1,
perPage: 40,
libraryPanels: [
elements: [
{
id: 1,
name: 'Library Panel Name',
kind: LibraryElementKind.Panel,
uid: 'uid',
description: 'Library Panel Description',
folderId: 0,
@ -215,7 +216,6 @@ describe('LibraryPanelsSearch', () => {
orgId: 1,
version: 1,
meta: {
canEdit: true,
folderName: 'General',
folderUid: '',
connectedDashboards: 0,
@ -247,10 +247,11 @@ describe('LibraryPanelsSearch', () => {
page: 1,
totalCount: 1,
perPage: 40,
libraryPanels: [
elements: [
{
id: 1,
name: 'Library Panel Name',
kind: LibraryElementKind.Panel,
uid: 'uid',
description: 'Library Panel Description',
folderId: 0,
@ -259,7 +260,6 @@ describe('LibraryPanelsSearch', () => {
orgId: 1,
version: 1,
meta: {
canEdit: true,
folderName: 'General',
folderUid: '',
connectedDashboards: 0,

View File

@ -7,7 +7,7 @@ import { SortPicker } from '../../../../core/components/Select/SortPicker';
import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter';
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../core/constants';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { FolderFilter } from '../../../../core/components/FolderFilter/FolderFilter';
import { FolderInfo } from '../../../../types';
import {
@ -25,7 +25,7 @@ export enum LibraryPanelsSearchVariant {
}
export interface LibraryPanelsSearchProps {
onClick: (panel: LibraryPanelDTO) => void;
onClick: (panel: LibraryElementDTO) => void;
variant?: LibraryPanelsSearchVariant;
showSort?: boolean;
showPanelFilter?: boolean;

View File

@ -5,13 +5,13 @@ import { Pagination, useStyles } from '@grafana/ui';
import { GrafanaTheme, LoadingState } from '@grafana/data';
import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { changePage, initialLibraryPanelsViewState, libraryPanelsViewReducer } from './reducer';
import { asyncDispatcher, deleteLibraryPanel, searchForLibraryPanels } from './actions';
interface LibraryPanelViewProps {
className?: string;
onClickCard: (panel: LibraryPanelDTO) => void;
onClickCard: (panel: LibraryElementDTO) => void;
showSecondaryActions?: boolean;
currentPanelId?: string;
searchString: string;
@ -58,7 +58,7 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
300,
[searchString, sortDirection, panelFilter, folderFilter, page, asyncDispatch]
);
const onDelete = ({ uid }: LibraryPanelDTO) =>
const onDelete = ({ uid }: LibraryElementDTO) =>
asyncDispatch(deleteLibraryPanel(uid, { searchString, page, perPage }));
const onPageChange = (page: number) => asyncDispatch(changePage({ page }));

View File

@ -27,11 +27,11 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
page: args.page,
excludeUid: args.currentPanelId,
sortDirection: args.sortDirection,
panelFilter: args.panelFilter,
typeFilter: args.panelFilter,
folderFilter: args.folderFilter,
})
).pipe(
mergeMap(({ perPage, libraryPanels, page, totalCount }) =>
mergeMap(({ perPage, elements: libraryPanels, page, totalCount }) =>
of(searchCompleted({ libraryPanels, page, perPage, totalCount }))
),
catchError((err) => {

View File

@ -9,7 +9,7 @@ import {
LibraryPanelsViewState,
searchCompleted,
} from './reducer';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO, LibraryElementKind } from '../../types';
describe('libraryPanelsViewReducer', () => {
describe('when initSearch is dispatched', () => {
@ -85,8 +85,8 @@ describe('libraryPanelsViewReducer', () => {
});
});
function getLibraryPanelMocks(count: number): LibraryPanelDTO[] {
const mocks: LibraryPanelDTO[] = [];
function getLibraryPanelMocks(count: number): LibraryElementDTO[] {
const mocks: LibraryElementDTO[] = [];
for (let i = 0; i < count; i++) {
mocks.push(
@ -109,7 +109,6 @@ function mockLibraryPanel({
name = 'Test Panel',
model = { type: 'text', title: 'Test Panel' },
meta = {
canEdit: true,
folderName: 'General',
folderUid: '',
connectedDashboards: 0,
@ -121,13 +120,14 @@ function mockLibraryPanel({
version = 1,
description = 'a description',
type = 'text',
}: Partial<LibraryPanelDTO> = {}): LibraryPanelDTO {
}: Partial<LibraryElementDTO> = {}): LibraryElementDTO {
return {
uid,
id,
orgId,
folderId,
name,
kind: LibraryElementKind.Panel,
model,
version,
meta,

View File

@ -1,12 +1,12 @@
import { createAction } from '@reduxjs/toolkit';
import { LoadingState } from '@grafana/data';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { AnyAction } from 'redux';
export interface LibraryPanelsViewState {
loadingState: LoadingState;
libraryPanels: LibraryPanelDTO[];
libraryPanels: LibraryElementDTO[];
totalCount: number;
perPage: number;
page: number;

View File

@ -4,14 +4,14 @@ import { AsyncSelect, Button, Modal, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2, SelectableValue, urlUtil } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { DashboardSearchHit } from '../../../search/types';
import { getConnectedDashboards, getLibraryPanelConnectedDashboards } from '../../state/api';
import { debounce } from 'lodash';
export interface OpenLibraryPanelModalProps {
onDismiss: () => void;
libraryPanel: LibraryPanelDTO;
libraryPanel: LibraryElementDTO;
}
export function OpenLibraryPanelModal({ libraryPanel, onDismiss }: OpenLibraryPanelModalProps): JSX.Element {
@ -47,8 +47,11 @@ export function OpenLibraryPanelModal({ libraryPanel, onDismiss }: OpenLibraryPa
{connected > 0 ? (
<>
<p>
This panel is being used in <strong>{connected} dashboards</strong>.Please choose which dashboard to view
the panel in:
This panel is being used in{' '}
<strong>
{connected} {connected > 1 ? 'dashboards' : 'dashboard'}
</strong>
.Please choose which dashboard to view the panel in:
</p>
<AsyncSelect
isClearable

View File

@ -8,7 +8,7 @@ import { PanelModel } from 'app/features/dashboard/state';
import { AddLibraryPanelModal } from '../AddLibraryPanelModal/AddLibraryPanelModal';
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
import { PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events';
import { LibraryPanelDTO } from '../../types';
import { LibraryElementDTO } from '../../types';
import { toPanelModelLibraryPanel } from '../../utils';
import { changePanelPlugin } from 'app/features/dashboard/state/actions';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
@ -23,7 +23,7 @@ interface Props {
export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
const styles = useStyles2(getStyles);
const [showingAddPanelModal, setShowingAddPanelModal] = useState(false);
const [changeToPanel, setChangeToPanel] = useState<LibraryPanelDTO | undefined>(undefined);
const [changeToPanel, setChangeToPanel] = useState<LibraryElementDTO | undefined>(undefined);
const [panelFilter, setPanelFilter] = useState<string[]>([]);
const onPanelFilterChange = useCallback(
(plugins: PanelPluginMeta[]) => {
@ -63,7 +63,7 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
setShowingAddPanelModal(true);
};
const onChangeLibraryPanel = (panel: LibraryPanelDTO) => {
const onChangeLibraryPanel = (panel: LibraryElementDTO) => {
setChangeToPanel(panel);
};

View File

@ -1,4 +1,10 @@
import { LibraryPanelDTO, LibraryPanelSearchResult, PanelModelWithLibraryPanel } from '../types';
import {
LibraryElementConnectionDTO,
LibraryElementDTO,
LibraryElementKind,
LibraryElementsSearchResult,
PanelModelWithLibraryPanel,
} from '../types';
import { DashboardSearchHit } from '../../search/types';
import { getBackendSrv } from '../../../core/services/backend_srv';
@ -8,7 +14,7 @@ export interface GetLibraryPanelsOptions {
page?: number;
excludeUid?: string;
sortDirection?: string;
panelFilter?: string[];
typeFilter?: string[];
folderFilter?: string[];
}
@ -18,35 +24,39 @@ export async function getLibraryPanels({
page = 1,
excludeUid = '',
sortDirection = '',
panelFilter = [],
typeFilter = [],
folderFilter = [],
}: GetLibraryPanelsOptions = {}): Promise<LibraryPanelSearchResult> {
}: GetLibraryPanelsOptions = {}): Promise<LibraryElementsSearchResult> {
const params = new URLSearchParams();
params.append('searchString', searchString);
params.append('sortDirection', sortDirection);
params.append('panelFilter', panelFilter.join(','));
params.append('typeFilter', typeFilter.join(','));
params.append('folderFilter', folderFilter.join(','));
params.append('excludeUid', excludeUid);
params.append('perPage', perPage.toString(10));
params.append('page', page.toString(10));
params.append('kind', LibraryElementKind.Panel.toString(10));
const { result } = await getBackendSrv().get(`/api/library-panels?${params.toString()}`);
const { result } = await getBackendSrv().get<{ result: LibraryElementsSearchResult }>(
`/api/library-elements?${params.toString()}`
);
return result;
}
export async function getLibraryPanel(uid: string): Promise<LibraryPanelDTO> {
const { result } = await getBackendSrv().get(`/api/library-panels/${uid}`);
export async function getLibraryPanel(uid: string): Promise<LibraryElementDTO> {
const { result } = await getBackendSrv().get(`/api/library-elements/${uid}`);
return result;
}
export async function addLibraryPanel(
panelSaveModel: PanelModelWithLibraryPanel,
folderId: number
): Promise<LibraryPanelDTO> {
const { result } = await getBackendSrv().post(`/api/library-panels`, {
): Promise<LibraryElementDTO> {
const { result } = await getBackendSrv().post(`/api/library-elements`, {
folderId,
name: panelSaveModel.title,
model: panelSaveModel,
kind: LibraryElementKind.Panel,
});
return result;
}
@ -54,31 +64,36 @@ export async function addLibraryPanel(
export async function updateLibraryPanel(
panelSaveModel: PanelModelWithLibraryPanel,
folderId: number
): Promise<LibraryPanelDTO> {
const { result } = await getBackendSrv().patch(`/api/library-panels/${panelSaveModel.libraryPanel.uid}`, {
): Promise<LibraryElementDTO> {
const { result } = await getBackendSrv().patch(`/api/library-elements/${panelSaveModel.libraryPanel.uid}`, {
folderId,
name: panelSaveModel.title,
model: panelSaveModel,
version: panelSaveModel.libraryPanel.version,
kind: LibraryElementKind.Panel,
});
return result;
}
export function deleteLibraryPanel(uid: string): Promise<{ message: string }> {
return getBackendSrv().delete(`/api/library-panels/${uid}`);
return getBackendSrv().delete(`/api/library-elements/${uid}`);
}
export async function getLibraryPanelConnectedDashboards(libraryPanelUid: string): Promise<number[]> {
const { result } = await getBackendSrv().get(`/api/library-panels/${libraryPanelUid}/dashboards`);
export async function getLibraryPanelConnectedDashboards(
libraryPanelUid: string
): Promise<LibraryElementConnectionDTO[]> {
const { result } = await getBackendSrv().get<{ result: LibraryElementConnectionDTO[] }>(
`/api/library-elements/${libraryPanelUid}/connections`
);
return result;
}
export async function getConnectedDashboards(uid: string): Promise<DashboardSearchHit[]> {
const dashboardIds = await getLibraryPanelConnectedDashboards(uid);
if (dashboardIds.length === 0) {
const connections = await getLibraryPanelConnectedDashboards(uid);
if (connections.length === 0) {
return [];
}
const searchHits = await getBackendSrv().search({ dashboardIds });
const searchHits = await getBackendSrv().search({ dashboardIds: connections.map((c) => c.connectionId) });
return searchHits;
}

View File

@ -2,44 +2,62 @@ import { PanelModel } from '../dashboard/state';
import { Dispatch } from 'react';
import { AnyAction } from '@reduxjs/toolkit';
export interface LibraryPanelSearchResult {
export enum LibraryElementKind {
Panel = 1,
Variable,
}
export enum LibraryElementConnectionKind {
Dashboard = 1,
}
export interface LibraryElementConnectionDTO {
id: number;
kind: LibraryElementConnectionKind;
elementId: number;
connectionId: number;
created: string;
createdBy: LibraryElementDTOMetaUser;
}
export interface LibraryElementsSearchResult {
totalCount: number;
libraryPanels: LibraryPanelDTO[];
elements: LibraryElementDTO[];
perPage: number;
page: number;
}
export interface LibraryPanelDTO {
export interface LibraryElementDTO {
id: number;
orgId: number;
folderId: number;
uid: string;
name: string;
kind: LibraryElementKind;
type: string;
description: string;
model: any;
version: number;
meta: LibraryPanelDTOMeta;
meta: LibraryElementDTOMeta;
}
export interface LibraryPanelDTOMeta {
canEdit: boolean;
export interface LibraryElementDTOMeta {
folderName: string;
folderUid: string;
connectedDashboards: number;
created: string;
updated: string;
createdBy: LibraryPanelDTOMetaUser;
updatedBy: LibraryPanelDTOMetaUser;
createdBy: LibraryElementDTOMetaUser;
updatedBy: LibraryElementDTOMetaUser;
}
export interface LibraryPanelDTOMetaUser {
export interface LibraryElementDTOMetaUser {
id: number;
name: string;
avatarUrl: string;
}
export type PanelModelLibraryPanel = Pick<LibraryPanelDTO, 'uid' | 'name' | 'meta' | 'version'>;
export type PanelModelLibraryPanel = Pick<LibraryElementDTO, 'uid' | 'name' | 'meta' | 'version'>;
export interface PanelModelWithLibraryPanel extends PanelModel {
libraryPanel: PanelModelLibraryPanel;

View File

@ -1,4 +1,4 @@
import { LibraryPanelDTO, PanelModelLibraryPanel } from './types';
import { LibraryElementDTO, PanelModelLibraryPanel } from './types';
import { PanelModel } from '../dashboard/state';
import { addLibraryPanel, updateLibraryPanel } from './state/api';
import { createErrorNotification, createSuccessNotification } from '../../core/copy/appNotification';
@ -12,12 +12,12 @@ export function createPanelLibrarySuccessNotification(message: string): AppNotif
return createSuccessNotification(message);
}
export function toPanelModelLibraryPanel(libraryPanelDto: LibraryPanelDTO): PanelModelLibraryPanel {
export function toPanelModelLibraryPanel(libraryPanelDto: LibraryElementDTO): PanelModelLibraryPanel {
const { uid, name, meta, version } = libraryPanelDto;
return { uid, name, meta, version };
}
export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise<LibraryPanelDTO> {
export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise<LibraryElementDTO> {
const panelSaveModel = toPanelSaveModel(panel);
const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderId);
updatePanelModelWithUpdate(panel, savedPanel);
@ -37,7 +37,7 @@ function toPanelSaveModel(panel: PanelModel): any {
return panelSaveModel;
}
function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryPanelDTO): void {
function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryElementDTO): void {
panel.restoreModel({
...updated.model,
configRev: 0, // reset config rev, since changes have been saved
@ -46,7 +46,7 @@ function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryPanelDTO)
panel.refresh();
}
function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise<LibraryPanelDTO> {
function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise<LibraryElementDTO> {
if (!panel.libraryPanel) {
return Promise.reject();
}