mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LibraryElements: Adds library elements api and tables (#33741)
* WIP: intial structure * Refactor: adds create library element endpoint * Feature: adds delete library element * wip * Refactor: adds get api * Refactor: adds get all api * Refactor: adds patch api * Refactor: changes to library_element_connection * Refactor: add get connections api * wip: in the middle of refactor * wip * Refactor: consolidating both api:s * Refactor: points front end to library elements api * Tests: Fixes broken test * Fix: fixes delete library elements in folder and adds tests * Refactor: changes order of tabs in manage folder * Refactor: fixes so link does not cover whole card * Update pkg/services/libraryelements/libraryelements.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/libraryelements/libraryelements_permissions_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/libraryelements/database.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Chore: changes after PR comments * Update libraryelements.go * Chore: updates after PR comments Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
parent
9b12e79f3e
commit
f1b2c750e5
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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:""`
|
||||
|
123
pkg/services/libraryelements/api.go
Normal file
123
pkg/services/libraryelements/api.go
Normal file
@ -0,0 +1,123 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func (l *LibraryElementService) registerAPIEndpoints() {
|
||||
if !l.IsEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
l.RouteRegister.Group("/api/library-elements", func(entities routing.RouteRegister) {
|
||||
entities.Post("/", middleware.ReqSignedIn, binding.Bind(CreateLibraryElementCommand{}), routing.Wrap(l.createHandler))
|
||||
entities.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(l.deleteHandler))
|
||||
entities.Get("/", middleware.ReqSignedIn, routing.Wrap(l.getAllHandler))
|
||||
entities.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(l.getHandler))
|
||||
entities.Get("/:uid/connections/", middleware.ReqSignedIn, routing.Wrap(l.getConnectionsHandler))
|
||||
entities.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryElementCommand{}), routing.Wrap(l.patchHandler))
|
||||
})
|
||||
}
|
||||
|
||||
// createHandler handles POST /api/library-elements.
|
||||
func (l *LibraryElementService) createHandler(c *models.ReqContext, cmd CreateLibraryElementCommand) response.Response {
|
||||
element, err := l.createLibraryElement(c, cmd)
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, "Failed to create library element")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": element})
|
||||
}
|
||||
|
||||
// deleteHandler handles DELETE /api/library-elements/:uid.
|
||||
func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Response {
|
||||
err := l.deleteLibraryElement(c, c.Params(":uid"))
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, "Failed to delete library element")
|
||||
}
|
||||
|
||||
return response.Success("Library element deleted")
|
||||
}
|
||||
|
||||
// getHandler handles GET /api/library-elements/:uid.
|
||||
func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Response {
|
||||
element, err := l.getLibraryElement(c, c.Params(":uid"))
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, "Failed to get library element")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": element})
|
||||
}
|
||||
|
||||
// getAllHandler handles GET /api/library-elements/.
|
||||
func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Response {
|
||||
query := searchLibraryElementsQuery{
|
||||
perPage: c.QueryInt("perPage"),
|
||||
page: c.QueryInt("page"),
|
||||
searchString: c.Query("searchString"),
|
||||
sortDirection: c.Query("sortDirection"),
|
||||
kind: c.QueryInt("kind"),
|
||||
typeFilter: c.Query("typeFilter"),
|
||||
excludeUID: c.Query("excludeUid"),
|
||||
folderFilter: c.Query("folderFilter"),
|
||||
}
|
||||
elementsResult, err := l.getAllLibraryElements(c, query)
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, "Failed to get library elements")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": elementsResult})
|
||||
}
|
||||
|
||||
// patchHandler handles PATCH /api/library-elements/:uid
|
||||
func (l *LibraryElementService) patchHandler(c *models.ReqContext, cmd patchLibraryElementCommand) response.Response {
|
||||
element, err := l.patchLibraryElement(c, cmd, c.Params(":uid"))
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, "Failed to update library element")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": element})
|
||||
}
|
||||
|
||||
// getConnectionsHandler handles GET /api/library-panels/:uid/connections/.
|
||||
func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) response.Response {
|
||||
connections, err := l.getConnections(c, c.Params(":uid"))
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, "Failed to get connections")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": connections})
|
||||
}
|
||||
|
||||
func toLibraryElementError(err error, message string) response.Response {
|
||||
if errors.Is(err, errLibraryElementAlreadyExists) {
|
||||
return response.Error(400, errLibraryElementAlreadyExists.Error(), err)
|
||||
}
|
||||
if errors.Is(err, errLibraryElementNotFound) {
|
||||
return response.Error(404, errLibraryElementNotFound.Error(), err)
|
||||
}
|
||||
if errors.Is(err, errLibraryElementDashboardNotFound) {
|
||||
return response.Error(404, errLibraryElementDashboardNotFound.Error(), err)
|
||||
}
|
||||
if errors.Is(err, errLibraryElementVersionMismatch) {
|
||||
return response.Error(412, errLibraryElementVersionMismatch.Error(), err)
|
||||
}
|
||||
if errors.Is(err, models.ErrFolderNotFound) {
|
||||
return response.Error(404, models.ErrFolderNotFound.Error(), err)
|
||||
}
|
||||
if errors.Is(err, models.ErrFolderAccessDenied) {
|
||||
return response.Error(403, models.ErrFolderAccessDenied.Error(), err)
|
||||
}
|
||||
if errors.Is(err, errLibraryElementHasConnections) {
|
||||
return response.Error(403, errLibraryElementHasConnections.Error(), err)
|
||||
}
|
||||
return response.Error(500, message, err)
|
||||
}
|
688
pkg/services/libraryelements/database.go
Normal file
688
pkg/services/libraryelements/database.go
Normal file
@ -0,0 +1,688 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
selectLibraryElementDTOWithMeta = `
|
||||
SELECT DISTINCT
|
||||
le.name, le.id, le.org_id, le.folder_id, le.uid, le.kind, le.type, le.description, le.model, le.created, le.created_by, le.updated, le.updated_by, le.version
|
||||
, u1.login AS created_by_name
|
||||
, u1.email AS created_by_email
|
||||
, u2.login AS updated_by_name
|
||||
, u2.email AS updated_by_email
|
||||
, (SELECT COUNT(connection_id) FROM ` + connectionTableName + ` WHERE library_element_id = le.id AND connection_kind=1) AS connections`
|
||||
fromLibraryElementDTOWithMeta = `
|
||||
FROM library_element AS le
|
||||
LEFT JOIN user AS u1 ON le.created_by = u1.id
|
||||
LEFT JOIN user AS u2 ON le.updated_by = u2.id
|
||||
`
|
||||
)
|
||||
|
||||
func syncFieldsWithModel(libraryElement *LibraryElement) error {
|
||||
var model map[string]interface{}
|
||||
if err := json.Unmarshal(libraryElement.Model, &model); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if LibraryElementKind(libraryElement.Kind) == Panel {
|
||||
model["title"] = libraryElement.Name
|
||||
} else if LibraryElementKind(libraryElement.Kind) == Variable {
|
||||
model["name"] = libraryElement.Name
|
||||
}
|
||||
if model["type"] != nil {
|
||||
libraryElement.Type = model["type"].(string)
|
||||
} else {
|
||||
model["type"] = libraryElement.Type
|
||||
}
|
||||
if model["description"] != nil {
|
||||
libraryElement.Description = model["description"].(string)
|
||||
} else {
|
||||
model["description"] = libraryElement.Description
|
||||
}
|
||||
syncedModel, err := json.Marshal(&model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
libraryElement.Model = syncedModel
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLibraryElement(session *sqlstore.DBSession, uid string, orgID int64) (LibraryElementWithMeta, error) {
|
||||
elements := make([]LibraryElementWithMeta, 0)
|
||||
sql := selectLibraryElementDTOWithMeta +
|
||||
", coalesce(dashboard.title, 'General') AS folder_name" +
|
||||
", coalesce(dashboard.uid, '') AS folder_uid" +
|
||||
fromLibraryElementDTOWithMeta +
|
||||
" LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" +
|
||||
" WHERE le.uid=? AND le.org_id=?"
|
||||
sess := session.SQL(sql, uid, orgID)
|
||||
err := sess.Find(&elements)
|
||||
if err != nil {
|
||||
return LibraryElementWithMeta{}, err
|
||||
}
|
||||
if len(elements) == 0 {
|
||||
return LibraryElementWithMeta{}, errLibraryElementNotFound
|
||||
}
|
||||
if len(elements) > 1 {
|
||||
return LibraryElementWithMeta{}, fmt.Errorf("found %d elements, while expecting at most one", len(elements))
|
||||
}
|
||||
|
||||
return elements[0], nil
|
||||
}
|
||||
|
||||
// createLibraryElement adds a library element.
|
||||
func (l *LibraryElementService) createLibraryElement(c *models.ReqContext, cmd CreateLibraryElementCommand) (LibraryElementDTO, error) {
|
||||
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
||||
return LibraryElementDTO{}, err
|
||||
}
|
||||
element := LibraryElement{
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
FolderID: cmd.FolderID,
|
||||
UID: util.GenerateShortUID(),
|
||||
Name: cmd.Name,
|
||||
Model: cmd.Model,
|
||||
Version: 1,
|
||||
Kind: cmd.Kind,
|
||||
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
|
||||
CreatedBy: c.SignedInUser.UserId,
|
||||
UpdatedBy: c.SignedInUser.UserId,
|
||||
}
|
||||
|
||||
if err := syncFieldsWithModel(&element); err != nil {
|
||||
return LibraryElementDTO{}, err
|
||||
}
|
||||
|
||||
err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
if err := l.requirePermissionsOnFolder(c.SignedInUser, cmd.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := session.Insert(&element); err != nil {
|
||||
if l.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
|
||||
return errLibraryElementAlreadyExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
dto := LibraryElementDTO{
|
||||
ID: element.ID,
|
||||
OrgID: element.OrgID,
|
||||
FolderID: element.FolderID,
|
||||
UID: element.UID,
|
||||
Name: element.Name,
|
||||
Kind: element.Kind,
|
||||
Type: element.Type,
|
||||
Description: element.Description,
|
||||
Model: element.Model,
|
||||
Version: element.Version,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
Connections: 0,
|
||||
Created: element.Created,
|
||||
Updated: element.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: element.CreatedBy,
|
||||
Name: c.SignedInUser.Login,
|
||||
AvatarURL: dtos.GetGravatarUrl(c.SignedInUser.Email),
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: element.UpdatedBy,
|
||||
Name: c.SignedInUser.Login,
|
||||
AvatarURL: dtos.GetGravatarUrl(c.SignedInUser.Email),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return dto, err
|
||||
}
|
||||
|
||||
// deleteLibraryElement deletes a library element.
|
||||
func (l *LibraryElementService) deleteLibraryElement(c *models.ReqContext, uid string) error {
|
||||
return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
element, err := getLibraryElement(session, uid, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.requirePermissionsOnFolder(c.SignedInUser, element.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
var connectionIDs []struct {
|
||||
ConnectionID int64 `xorm:"connection_id"`
|
||||
}
|
||||
sql := "SELECT connection_id FROM library_element_connection WHERE library_element_id=?"
|
||||
if err := session.SQL(sql, element.ID).Find(&connectionIDs); err != nil {
|
||||
return err
|
||||
} else if len(connectionIDs) > 0 {
|
||||
return errLibraryElementHasConnections
|
||||
}
|
||||
|
||||
result, err := session.Exec("DELETE FROM library_element WHERE id=?", element.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
} else if rowsAffected != 1 {
|
||||
return errLibraryElementNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// getLibraryElement gets a Library Element.
|
||||
func (l *LibraryElementService) getLibraryElement(c *models.ReqContext, uid string) (LibraryElementDTO, error) {
|
||||
var libraryElement LibraryElementWithMeta
|
||||
err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
libraryElements := make([]LibraryElementWithMeta, 0)
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
builder.Write(selectLibraryElementDTOWithMeta)
|
||||
builder.Write(", 'General' as folder_name ")
|
||||
builder.Write(", '' as folder_uid ")
|
||||
builder.Write(fromLibraryElementDTOWithMeta)
|
||||
builder.Write(` WHERE le.uid=? AND le.org_id=? AND le.folder_id=0`, uid, c.SignedInUser.OrgId)
|
||||
builder.Write(" UNION ")
|
||||
builder.Write(selectLibraryElementDTOWithMeta)
|
||||
builder.Write(", dashboard.title as folder_name ")
|
||||
builder.Write(", dashboard.uid as folder_uid ")
|
||||
builder.Write(fromLibraryElementDTOWithMeta)
|
||||
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0")
|
||||
builder.Write(` WHERE le.uid=? AND le.org_id=?`, uid, c.SignedInUser.OrgId)
|
||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||
}
|
||||
builder.Write(` OR dashboard.id=0`)
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElements); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(libraryElements) == 0 {
|
||||
return errLibraryElementNotFound
|
||||
}
|
||||
if len(libraryElements) > 1 {
|
||||
return fmt.Errorf("found %d elements, while expecting at most one", len(libraryElements))
|
||||
}
|
||||
|
||||
libraryElement = libraryElements[0]
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return LibraryElementDTO{}, err
|
||||
}
|
||||
|
||||
dto := LibraryElementDTO{
|
||||
ID: libraryElement.ID,
|
||||
OrgID: libraryElement.OrgID,
|
||||
FolderID: libraryElement.FolderID,
|
||||
UID: libraryElement.UID,
|
||||
Name: libraryElement.Name,
|
||||
Kind: libraryElement.Kind,
|
||||
Type: libraryElement.Type,
|
||||
Description: libraryElement.Description,
|
||||
Model: libraryElement.Model,
|
||||
Version: libraryElement.Version,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
FolderName: libraryElement.FolderName,
|
||||
FolderUID: libraryElement.FolderUID,
|
||||
Connections: libraryElement.Connections,
|
||||
Created: libraryElement.Created,
|
||||
Updated: libraryElement.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: libraryElement.CreatedBy,
|
||||
Name: libraryElement.CreatedByName,
|
||||
AvatarURL: dtos.GetGravatarUrl(libraryElement.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: libraryElement.UpdatedBy,
|
||||
Name: libraryElement.UpdatedByName,
|
||||
AvatarURL: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
// getAllLibraryElements gets all Library Elements.
|
||||
func (l *LibraryElementService) getAllLibraryElements(c *models.ReqContext, query searchLibraryElementsQuery) (LibraryElementSearchResult, error) {
|
||||
elements := make([]LibraryElementWithMeta, 0)
|
||||
result := LibraryElementSearchResult{}
|
||||
if query.perPage <= 0 {
|
||||
query.perPage = 100
|
||||
}
|
||||
if query.page <= 0 {
|
||||
query.page = 1
|
||||
}
|
||||
var typeFilter []string
|
||||
if len(strings.TrimSpace(query.typeFilter)) > 0 {
|
||||
typeFilter = strings.Split(query.typeFilter, ",")
|
||||
}
|
||||
folderFilter := parseFolderFilter(query)
|
||||
if folderFilter.parseError != nil {
|
||||
return LibraryElementSearchResult{}, folderFilter.parseError
|
||||
}
|
||||
err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
if folderFilter.includeGeneralFolder {
|
||||
builder.Write(selectLibraryElementDTOWithMeta)
|
||||
builder.Write(", 'General' as folder_name ")
|
||||
builder.Write(", '' as folder_uid ")
|
||||
builder.Write(fromLibraryElementDTOWithMeta)
|
||||
builder.Write(` WHERE le.org_id=? AND le.folder_id=0`, c.SignedInUser.OrgId)
|
||||
writeKindSQL(query, &builder)
|
||||
writeSearchStringSQL(query, l.SQLStore, &builder)
|
||||
writeExcludeSQL(query, &builder)
|
||||
writeTypeFilterSQL(typeFilter, &builder)
|
||||
builder.Write(" UNION ")
|
||||
}
|
||||
builder.Write(selectLibraryElementDTOWithMeta)
|
||||
builder.Write(", dashboard.title as folder_name ")
|
||||
builder.Write(", dashboard.uid as folder_uid ")
|
||||
builder.Write(fromLibraryElementDTOWithMeta)
|
||||
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id<>0")
|
||||
builder.Write(` WHERE le.org_id=?`, c.SignedInUser.OrgId)
|
||||
writeKindSQL(query, &builder)
|
||||
writeSearchStringSQL(query, l.SQLStore, &builder)
|
||||
writeExcludeSQL(query, &builder)
|
||||
writeTypeFilterSQL(typeFilter, &builder)
|
||||
if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||
}
|
||||
if query.sortDirection == search.SortAlphaDesc.Name {
|
||||
builder.Write(" ORDER BY 1 DESC")
|
||||
} else {
|
||||
builder.Write(" ORDER BY 1 ASC")
|
||||
}
|
||||
writePerPageSQL(query, l.SQLStore, &builder)
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&elements); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
retDTOs := make([]LibraryElementDTO, 0)
|
||||
for _, element := range elements {
|
||||
retDTOs = append(retDTOs, LibraryElementDTO{
|
||||
ID: element.ID,
|
||||
OrgID: element.OrgID,
|
||||
FolderID: element.FolderID,
|
||||
UID: element.UID,
|
||||
Name: element.Name,
|
||||
Kind: element.Kind,
|
||||
Type: element.Type,
|
||||
Description: element.Description,
|
||||
Model: element.Model,
|
||||
Version: element.Version,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
FolderName: element.FolderName,
|
||||
FolderUID: element.FolderUID,
|
||||
Connections: element.Connections,
|
||||
Created: element.Created,
|
||||
Updated: element.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: element.CreatedBy,
|
||||
Name: element.CreatedByName,
|
||||
AvatarURL: dtos.GetGravatarUrl(element.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: element.UpdatedBy,
|
||||
Name: element.UpdatedByName,
|
||||
AvatarURL: dtos.GetGravatarUrl(element.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
var libraryElements []LibraryElement
|
||||
countBuilder := sqlstore.SQLBuilder{}
|
||||
countBuilder.Write("SELECT * FROM library_element AS le")
|
||||
countBuilder.Write(` WHERE le.org_id=?`, c.SignedInUser.OrgId)
|
||||
writeKindSQL(query, &countBuilder)
|
||||
writeSearchStringSQL(query, l.SQLStore, &countBuilder)
|
||||
writeExcludeSQL(query, &countBuilder)
|
||||
writeTypeFilterSQL(typeFilter, &countBuilder)
|
||||
if err := folderFilter.writeFolderFilterSQL(true, &countBuilder); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&libraryElements); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = LibraryElementSearchResult{
|
||||
TotalCount: int64(len(libraryElements)),
|
||||
Elements: retDTOs,
|
||||
Page: query.page,
|
||||
PerPage: query.perPage,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) handleFolderIDPatches(elementToPatch *LibraryElement, fromFolderID int64, toFolderID int64, user *models.SignedInUser) error {
|
||||
// FolderID was not provided in the PATCH request
|
||||
if toFolderID == -1 {
|
||||
toFolderID = fromFolderID
|
||||
}
|
||||
|
||||
// FolderID was provided in the PATCH request
|
||||
if toFolderID != -1 && toFolderID != fromFolderID {
|
||||
if err := l.requirePermissionsOnFolder(user, toFolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Always check permissions for the folder where library element resides
|
||||
if err := l.requirePermissionsOnFolder(user, fromFolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
elementToPatch.FolderID = toFolderID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// patchLibraryElement updates a Library Element.
|
||||
func (l *LibraryElementService) patchLibraryElement(c *models.ReqContext, cmd patchLibraryElementCommand, uid string) (LibraryElementDTO, error) {
|
||||
var dto LibraryElementDTO
|
||||
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
||||
return LibraryElementDTO{}, err
|
||||
}
|
||||
err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
elementInDB, err := getLibraryElement(session, uid, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if elementInDB.Version != cmd.Version {
|
||||
return errLibraryElementVersionMismatch
|
||||
}
|
||||
|
||||
var libraryElement = LibraryElement{
|
||||
ID: elementInDB.ID,
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
FolderID: cmd.FolderID,
|
||||
UID: uid,
|
||||
Name: cmd.Name,
|
||||
Kind: elementInDB.Kind,
|
||||
Type: elementInDB.Type,
|
||||
Description: elementInDB.Description,
|
||||
Model: cmd.Model,
|
||||
Version: elementInDB.Version + 1,
|
||||
Created: elementInDB.Created,
|
||||
CreatedBy: elementInDB.CreatedBy,
|
||||
Updated: time.Now(),
|
||||
UpdatedBy: c.SignedInUser.UserId,
|
||||
}
|
||||
|
||||
if cmd.Name == "" {
|
||||
libraryElement.Name = elementInDB.Name
|
||||
}
|
||||
if cmd.Model == nil {
|
||||
libraryElement.Model = elementInDB.Model
|
||||
}
|
||||
if err := l.handleFolderIDPatches(&libraryElement, elementInDB.FolderID, cmd.FolderID, c.SignedInUser); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncFieldsWithModel(&libraryElement); err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected, err := session.ID(elementInDB.ID).Update(&libraryElement); err != nil {
|
||||
if l.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
|
||||
return errLibraryElementAlreadyExists
|
||||
}
|
||||
return err
|
||||
} else if rowsAffected != 1 {
|
||||
return errLibraryElementNotFound
|
||||
}
|
||||
|
||||
dto = LibraryElementDTO{
|
||||
ID: libraryElement.ID,
|
||||
OrgID: libraryElement.OrgID,
|
||||
FolderID: libraryElement.FolderID,
|
||||
UID: libraryElement.UID,
|
||||
Name: libraryElement.Name,
|
||||
Kind: libraryElement.Kind,
|
||||
Type: libraryElement.Type,
|
||||
Description: libraryElement.Description,
|
||||
Model: libraryElement.Model,
|
||||
Version: libraryElement.Version,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
Connections: elementInDB.Connections,
|
||||
Created: libraryElement.Created,
|
||||
Updated: libraryElement.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: elementInDB.CreatedBy,
|
||||
Name: elementInDB.CreatedByName,
|
||||
AvatarURL: dtos.GetGravatarUrl(elementInDB.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: libraryElement.UpdatedBy,
|
||||
Name: c.SignedInUser.Login,
|
||||
AvatarURL: dtos.GetGravatarUrl(c.SignedInUser.Email),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return dto, err
|
||||
}
|
||||
|
||||
// getConnections gets all connections for a Library Element.
|
||||
func (l *LibraryElementService) getConnections(c *models.ReqContext, uid string) ([]LibraryElementConnectionDTO, error) {
|
||||
connections := make([]LibraryElementConnectionDTO, 0)
|
||||
err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
element, err := getLibraryElement(session, uid, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var libraryElementConnections []libraryElementConnectionWithMeta
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
builder.Write("SELECT lec.*, u1.login AS created_by_name, u1.email AS created_by_email")
|
||||
builder.Write(" FROM " + connectionTableName + " AS lec")
|
||||
builder.Write(" LEFT JOIN user AS u1 ON lec.created_by = u1.id")
|
||||
builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id")
|
||||
builder.Write(` WHERE lec.library_element_id=?`, element.ID)
|
||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||
}
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElementConnections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, connection := range libraryElementConnections {
|
||||
connections = append(connections, LibraryElementConnectionDTO{
|
||||
ID: connection.ID,
|
||||
Kind: connection.ConnectionKind,
|
||||
ElementID: connection.LibraryElementID,
|
||||
ConnectionID: connection.ConnectionID,
|
||||
Created: connection.Created,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: connection.CreatedBy,
|
||||
Name: connection.CreatedByName,
|
||||
AvatarURL: dtos.GetGravatarUrl(connection.CreatedByEmail),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return connections, err
|
||||
}
|
||||
|
||||
//getElementsForDashboardID gets all elements for a specific dashboard
|
||||
func (l *LibraryElementService) getElementsForDashboardID(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error) {
|
||||
libraryElementMap := make(map[string]LibraryElementDTO)
|
||||
err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
var libraryElements []LibraryElementWithMeta
|
||||
sql := selectLibraryElementDTOWithMeta +
|
||||
", coalesce(dashboard.title, 'General') AS folder_name" +
|
||||
", coalesce(dashboard.uid, '') AS folder_uid" +
|
||||
fromLibraryElementDTOWithMeta +
|
||||
" LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" +
|
||||
" INNER JOIN " + connectionTableName + " AS lce ON lce.library_element_id = le.id AND lce.connection_kind=1 AND lce.connection_id=?"
|
||||
sess := session.SQL(sql, dashboardID)
|
||||
err := sess.Find(&libraryElements)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, element := range libraryElements {
|
||||
libraryElementMap[element.UID] = LibraryElementDTO{
|
||||
ID: element.ID,
|
||||
OrgID: element.OrgID,
|
||||
FolderID: element.FolderID,
|
||||
UID: element.UID,
|
||||
Name: element.Name,
|
||||
Kind: element.Kind,
|
||||
Type: element.Type,
|
||||
Description: element.Description,
|
||||
Model: element.Model,
|
||||
Version: element.Version,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
FolderName: element.FolderName,
|
||||
FolderUID: element.FolderUID,
|
||||
Connections: element.Connections,
|
||||
Created: element.Created,
|
||||
Updated: element.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: element.CreatedBy,
|
||||
Name: element.CreatedByName,
|
||||
AvatarURL: dtos.GetGravatarUrl(element.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: element.UpdatedBy,
|
||||
Name: element.UpdatedByName,
|
||||
AvatarURL: dtos.GetGravatarUrl(element.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return libraryElementMap, err
|
||||
}
|
||||
|
||||
// connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard.
|
||||
func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContext, elementUIDs []string, dashboardID int64) error {
|
||||
err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE connection_kind=1 AND connection_id=?", dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, elementUID := range elementUIDs {
|
||||
element, err := getLibraryElement(session, elementUID, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.requirePermissionsOnFolder(c.SignedInUser, element.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
connection := libraryElementConnection{
|
||||
LibraryElementID: element.ID,
|
||||
ConnectionKind: 1,
|
||||
ConnectionID: dashboardID,
|
||||
Created: time.Now(),
|
||||
CreatedBy: c.SignedInUser.UserId,
|
||||
}
|
||||
if _, err := session.Insert(&connection); err != nil {
|
||||
if l.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// disconnectElementsFromDashboardID deletes connections for all Library Elements in a Dashboard.
|
||||
func (l *LibraryElementService) disconnectElementsFromDashboardID(c *models.ReqContext, dashboardID int64) error {
|
||||
return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE connection_kind=1 AND connection_id=?", dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// deleteLibraryElementsInFolderUID deletes all Library Elements in a folder.
|
||||
func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c *models.ReqContext, folderUID string) error {
|
||||
return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
var folderUIDs []struct {
|
||||
ID int64 `xorm:"id"`
|
||||
}
|
||||
err := session.SQL("SELECT id from dashboard WHERE uid=? AND org_id=? AND is_folder=1", folderUID, c.SignedInUser.OrgId).Find(&folderUIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(folderUIDs) != 1 {
|
||||
return fmt.Errorf("found %d folders, while expecting at most one", len(folderUIDs))
|
||||
}
|
||||
folderID := folderUIDs[0].ID
|
||||
|
||||
if err := l.requirePermissionsOnFolder(c.SignedInUser, folderID); err != nil {
|
||||
return err
|
||||
}
|
||||
var connectionIDs []struct {
|
||||
ConnectionID int64 `xorm:"connection_id"`
|
||||
}
|
||||
sql := "SELECT lec.connection_id FROM library_element AS le"
|
||||
sql += " INNER JOIN " + connectionTableName + " AS lec on le.id = lec.library_element_id"
|
||||
sql += " WHERE le.folder_id=? AND le.org_id=?"
|
||||
err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&connectionIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(connectionIDs) > 0 {
|
||||
return ErrFolderHasConnectedLibraryElements
|
||||
}
|
||||
|
||||
var elementIDs []struct {
|
||||
ID int64 `xorm:"id"`
|
||||
}
|
||||
err = session.SQL("SELECT id from library_element WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId).Find(&elementIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, elementID := range elementIDs {
|
||||
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE library_element_id=?", elementID.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := session.Exec("DELETE FROM library_element WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
@ -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
|
139
pkg/services/libraryelements/libraryelements.go
Normal file
139
pkg/services/libraryelements/libraryelements.go
Normal file
@ -0,0 +1,139 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// LibraryElementService is the service for the Library Element feature.
|
||||
type LibraryElementService struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
SQLStore *sqlstore.SQLStore `inject:""`
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
const connectionTableName = "library_element_connection"
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&LibraryElementService{})
|
||||
}
|
||||
|
||||
// Init initializes the LibraryElement service
|
||||
func (l *LibraryElementService) Init() error {
|
||||
l.log = log.New("library-elements")
|
||||
|
||||
l.registerAPIEndpoints()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns true if the Panel Library feature is enabled for this instance.
|
||||
func (l *LibraryElementService) IsEnabled() bool {
|
||||
if l.Cfg == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return l.Cfg.IsPanelLibraryEnabled()
|
||||
}
|
||||
|
||||
// CreateElement creates a Library Element.
|
||||
func (l *LibraryElementService) CreateElement(c *models.ReqContext, cmd CreateLibraryElementCommand) (LibraryElementDTO, error) {
|
||||
if !l.IsEnabled() {
|
||||
return LibraryElementDTO{}, nil
|
||||
}
|
||||
|
||||
return l.createLibraryElement(c, cmd)
|
||||
}
|
||||
|
||||
// GetElementsForDashboard gets all connected elements for a specific dashboard.
|
||||
func (l *LibraryElementService) GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error) {
|
||||
if !l.IsEnabled() {
|
||||
return map[string]LibraryElementDTO{}, nil
|
||||
}
|
||||
|
||||
return l.getElementsForDashboardID(c, dashboardID)
|
||||
}
|
||||
|
||||
// ConnectElementsToDashboard connects elements to a specific dashboard.
|
||||
func (l *LibraryElementService) ConnectElementsToDashboard(c *models.ReqContext, elementUIDs []string, dashboardID int64) error {
|
||||
if !l.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return l.connectElementsToDashboardID(c, elementUIDs, dashboardID)
|
||||
}
|
||||
|
||||
// DisconnectElementsFromDashboard disconnects elements from a specific dashboard.
|
||||
func (l *LibraryElementService) DisconnectElementsFromDashboard(c *models.ReqContext, dashboardID int64) error {
|
||||
if !l.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return l.disconnectElementsFromDashboardID(c, dashboardID)
|
||||
}
|
||||
|
||||
// DeleteLibraryElementsInFolder deletes all elements for a specific folder.
|
||||
func (l *LibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error {
|
||||
if !l.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
return l.deleteLibraryElementsInFolderUID(c, folderUID)
|
||||
}
|
||||
|
||||
// AddMigration defines database migrations.
|
||||
// If Panel Library is not enabled does nothing.
|
||||
func (l *LibraryElementService) AddMigration(mg *migrator.Migrator) {
|
||||
if !l.IsEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
libraryElementsV1 := migrator.Table{
|
||||
Name: "library_element",
|
||||
Columns: []*migrator.Column{
|
||||
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: migrator.DB_BigInt, Nullable: false},
|
||||
{Name: "folder_id", Type: migrator.DB_BigInt, Nullable: false},
|
||||
{Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
|
||||
{Name: "name", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "kind", Type: migrator.DB_BigInt, Nullable: false},
|
||||
{Name: "type", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
|
||||
{Name: "description", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "model", Type: migrator.DB_Text, Nullable: false},
|
||||
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
|
||||
{Name: "created_by", Type: migrator.DB_BigInt, Nullable: false},
|
||||
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
|
||||
{Name: "updated_by", Type: migrator.DB_BigInt, Nullable: false},
|
||||
{Name: "version", Type: migrator.DB_BigInt, Nullable: false},
|
||||
},
|
||||
Indices: []*migrator.Index{
|
||||
{Cols: []string{"org_id", "folder_id", "name", "kind"}, Type: migrator.UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create library_element table v1", migrator.NewAddTableMigration(libraryElementsV1))
|
||||
mg.AddMigration("add index library_element", migrator.NewAddIndexMigration(libraryElementsV1, libraryElementsV1.Indices[0]))
|
||||
|
||||
libraryElementConnectionV1 := migrator.Table{
|
||||
Name: connectionTableName,
|
||||
Columns: []*migrator.Column{
|
||||
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "library_element_id", Type: migrator.DB_BigInt, Nullable: false},
|
||||
{Name: "connection_kind", Type: migrator.DB_BigInt, Nullable: false},
|
||||
{Name: "connection_id", Type: migrator.DB_BigInt, Nullable: false},
|
||||
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
|
||||
{Name: "created_by", Type: migrator.DB_BigInt, Nullable: false},
|
||||
},
|
||||
Indices: []*migrator.Index{
|
||||
{Cols: []string{"library_element_id", "connection_kind", "connection_id"}, Type: migrator.UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create "+connectionTableName+" table v1", migrator.NewAddTableMigration(libraryElementConnectionV1))
|
||||
mg.AddMigration("add index "+connectionTableName, migrator.NewAddIndexMigration(libraryElementConnectionV1, libraryElementConnectionV1.Indices[0]))
|
||||
}
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
76
pkg/services/libraryelements/libraryelements_delete_test.go
Normal file
76
pkg/services/libraryelements/libraryelements_delete_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestDeleteLibraryElement(t *testing.T) {
|
||||
scenarioWithPanel(t, "When an admin tries to delete a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
resp := sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to delete a library panel that exists, it should succeed",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to delete a library panel in another org, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
sc.reqContext.SignedInUser.OrgId = 2
|
||||
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
|
||||
resp := sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to delete a library panel that is connected, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
"name": sc.initialResult.Result.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing deleteHandler ",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
err := sc.service.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 403, resp.Status())
|
||||
})
|
||||
}
|
File diff suppressed because it is too large
Load Diff
75
pkg/services/libraryelements/libraryelements_get_test.go
Normal file
75
pkg/services/libraryelements/libraryelements_get_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestGetLibraryElement(t *testing.T) {
|
||||
scenarioWithPanel(t, "When an admin tries to get a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to get a library panel that exists, it should succeed and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
var expected = libraryElementResult{
|
||||
Result: libraryElement{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "A description",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryElementDTOMeta{
|
||||
FolderName: "ScenarioFolder",
|
||||
FolderUID: sc.folder.Uid,
|
||||
Connections: 0,
|
||||
Created: result.Result.Meta.Created,
|
||||
Updated: result.Result.Meta.Updated,
|
||||
CreatedBy: LibraryElementDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: userInDbName,
|
||||
AvatarURL: userInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryElementDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: userInDbName,
|
||||
AvatarURL: userInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to get a library panel that exists in an other org, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
sc.reqContext.SignedInUser.OrgId = 2
|
||||
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
353
pkg/services/libraryelements/libraryelements_test.go
Normal file
353
pkg/services/libraryelements/libraryelements_test.go
Normal file
@ -0,0 +1,353 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
dboards "github.com/grafana/grafana/pkg/dashboards"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const userInDbName = "user_in_db"
|
||||
const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad"
|
||||
|
||||
func TestDeleteLibraryPanelsInFolder(t *testing.T) {
|
||||
scenarioWithPanel(t, "When an admin tries to delete a folder that contains connected library elements, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
"name": sc.initialResult.Result.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing DeleteLibraryElementsInFolder",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
err := sc.service.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext, sc.folder.Uid)
|
||||
require.EqualError(t, err, ErrFolderHasConnectedLibraryElements.Error())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateVariableCommand(sc.folder.Id, "query0")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var result libraryElementsSearch
|
||||
err := json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result.Result)
|
||||
require.Equal(t, 2, len(result.Result.Elements))
|
||||
|
||||
err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext, sc.folder.Uid)
|
||||
require.NoError(t, err)
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result.Result)
|
||||
require.Equal(t, 0, len(result.Result.Elements))
|
||||
})
|
||||
}
|
||||
|
||||
type libraryElement struct {
|
||||
ID int64 `json:"id"`
|
||||
OrgID int64 `json:"orgId"`
|
||||
FolderID int64 `json:"folderId"`
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Kind int64 `json:"kind"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Model map[string]interface{} `json:"model"`
|
||||
Version int64 `json:"version"`
|
||||
Meta LibraryElementDTOMeta `json:"meta"`
|
||||
}
|
||||
|
||||
type libraryElementResult struct {
|
||||
Result libraryElement `json:"result"`
|
||||
}
|
||||
|
||||
type libraryElementsSearch struct {
|
||||
Result libraryElementsSearchResult `json:"result"`
|
||||
}
|
||||
|
||||
type libraryElementsSearchResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Elements []libraryElement `json:"elements"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
||||
func overrideLibraryElementServiceInRegistry(cfg *setting.Cfg) LibraryElementService {
|
||||
l := LibraryElementService{
|
||||
SQLStore: nil,
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
overrideServiceFunc := func(d registry.Descriptor) (*registry.Descriptor, bool) {
|
||||
descriptor := registry.Descriptor{
|
||||
Name: "LibraryElementService",
|
||||
Instance: &l,
|
||||
InitPriority: 0,
|
||||
}
|
||||
|
||||
return &descriptor, true
|
||||
}
|
||||
|
||||
registry.RegisterOverride(overrideServiceFunc)
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand {
|
||||
command := getCreateCommandWithModel(folderID, name, Panel, []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": 1,
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
"description": "A description"
|
||||
}
|
||||
`))
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementCommand {
|
||||
command := getCreateCommandWithModel(folderID, name, Variable, []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"name": "query0",
|
||||
"type": "query",
|
||||
"description": "A description"
|
||||
}
|
||||
`))
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func getCreateCommandWithModel(folderID int64, name string, kind LibraryElementKind, model []byte) CreateLibraryElementCommand {
|
||||
command := CreateLibraryElementCommand{
|
||||
FolderID: folderID,
|
||||
Name: name,
|
||||
Model: model,
|
||||
Kind: int64(kind),
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
ctx *macaron.Context
|
||||
service *LibraryElementService
|
||||
reqContext *models.ReqContext
|
||||
user models.SignedInUser
|
||||
folder *models.Folder
|
||||
initialResult libraryElementResult
|
||||
sqlStore *sqlstore.SQLStore
|
||||
}
|
||||
|
||||
type folderACLItem struct {
|
||||
roleType models.RoleType
|
||||
permission models.PermissionType
|
||||
}
|
||||
|
||||
func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard {
|
||||
dash.FolderId = folderID
|
||||
dashItem := &dashboards.SaveDashboardDTO{
|
||||
Dashboard: dash,
|
||||
Message: "",
|
||||
OrgId: user.OrgId,
|
||||
User: &user,
|
||||
Overwrite: false,
|
||||
}
|
||||
origUpdateAlerting := dashboards.UpdateAlerting
|
||||
t.Cleanup(func() {
|
||||
dashboards.UpdateAlerting = origUpdateAlerting
|
||||
})
|
||||
dashboards.UpdateAlerting = func(store dboards.Store, orgID int64, dashboard *models.Dashboard,
|
||||
user *models.SignedInUser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
dashboard, err := dashboards.NewService(sqlStore).SaveDashboard(dashItem, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
return dashboard
|
||||
}
|
||||
|
||||
func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string, user models.SignedInUser,
|
||||
items []folderACLItem) *models.Folder {
|
||||
t.Helper()
|
||||
|
||||
s := dashboards.NewFolderService(user.OrgId, &user, sqlStore)
|
||||
t.Logf("Creating folder with title and UID %q", title)
|
||||
folder, err := s.CreateFolder(title, title)
|
||||
require.NoError(t, err)
|
||||
|
||||
updateFolderACL(t, sqlStore, folder.Id, items)
|
||||
|
||||
return folder
|
||||
}
|
||||
|
||||
func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, items []folderACLItem) {
|
||||
t.Helper()
|
||||
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var aclItems []*models.DashboardAcl
|
||||
for _, item := range items {
|
||||
role := item.roleType
|
||||
permission := item.permission
|
||||
aclItems = append(aclItems, &models.DashboardAcl{
|
||||
DashboardID: folderID,
|
||||
Role: &role,
|
||||
Permission: permission,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
err := sqlStore.UpdateDashboardACL(folderID, aclItems)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryElementResult {
|
||||
t.Helper()
|
||||
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result = libraryElementResult{}
|
||||
err := json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
|
||||
t.Helper()
|
||||
|
||||
testScenario(t, desc, func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
sc.initialResult = validateAndUnMarshalResponse(t, resp)
|
||||
|
||||
fn(t, sc)
|
||||
})
|
||||
}
|
||||
|
||||
// testScenario is a wrapper around t.Run performing common setup for library panel tests.
|
||||
// It takes your real test function as a callback.
|
||||
func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
|
||||
t.Helper()
|
||||
|
||||
t.Run(desc, func(t *testing.T) {
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
ctx := macaron.Context{
|
||||
Req: macaron.Request{Request: &http.Request{}},
|
||||
}
|
||||
orgID := int64(1)
|
||||
role := models.ROLE_ADMIN
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
// Everything in this service is behind the feature toggle "panelLibrary"
|
||||
cfg.FeatureToggles = map[string]bool{"panelLibrary": true}
|
||||
// Because the LibraryElementService is behind a feature toggle, we need to override the service in the registry
|
||||
// with a Cfg that contains the feature toggle so migrations are run properly
|
||||
service := overrideLibraryElementServiceInRegistry(cfg)
|
||||
|
||||
// We need to assign SQLStore after the override and migrations are done
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
service.SQLStore = sqlStore
|
||||
|
||||
user := models.SignedInUser{
|
||||
UserId: 1,
|
||||
Name: "Signed In User",
|
||||
Login: "signed_in_user",
|
||||
Email: "signed.in.user@test.com",
|
||||
OrgId: orgID,
|
||||
OrgRole: role,
|
||||
LastSeenAt: time.Now(),
|
||||
}
|
||||
|
||||
// deliberate difference between signed in user and user in db to make it crystal clear
|
||||
// what to expect in the tests
|
||||
// In the real world these are identical
|
||||
cmd := models.CreateUserCommand{
|
||||
Email: "user.in.db@test.com",
|
||||
Name: "User In DB",
|
||||
Login: userInDbName,
|
||||
}
|
||||
_, err := sqlStore.CreateUser(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc := scenarioContext{
|
||||
user: user,
|
||||
ctx: &ctx,
|
||||
service: &service,
|
||||
sqlStore: sqlStore,
|
||||
reqContext: &models.ReqContext{
|
||||
Context: &ctx,
|
||||
SignedInUser: &user,
|
||||
},
|
||||
}
|
||||
|
||||
sc.folder = createFolderWithACL(t, sc.sqlStore, "ScenarioFolder", sc.user, []folderACLItem{})
|
||||
|
||||
fn(t, sc)
|
||||
})
|
||||
}
|
||||
|
||||
func getCompareOptions() []cmp.Option {
|
||||
return []cmp.Option{
|
||||
cmp.Transformer("Time", func(in time.Time) int64 {
|
||||
return in.UTC().Unix()
|
||||
}),
|
||||
}
|
||||
}
|
190
pkg/services/libraryelements/models.go
Normal file
190
pkg/services/libraryelements/models.go
Normal file
@ -0,0 +1,190 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LibraryElementKind int
|
||||
|
||||
const (
|
||||
Panel LibraryElementKind = iota + 1
|
||||
Variable
|
||||
)
|
||||
|
||||
type LibraryConnectionKind int
|
||||
|
||||
const (
|
||||
Dashboard LibraryConnectionKind = iota + 1
|
||||
)
|
||||
|
||||
// LibraryElement is the model for library element definitions.
|
||||
type LibraryElement struct {
|
||||
ID int64 `xorm:"pk autoincr 'id'"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
FolderID int64 `xorm:"folder_id"`
|
||||
UID string `xorm:"uid"`
|
||||
Name string
|
||||
Kind int64
|
||||
Type string
|
||||
Description string
|
||||
Model json.RawMessage
|
||||
Version int64
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
|
||||
CreatedBy int64
|
||||
UpdatedBy int64
|
||||
}
|
||||
|
||||
// LibraryElementWithMeta is the model used to retrieve entities with additional meta information.
|
||||
type LibraryElementWithMeta struct {
|
||||
ID int64 `xorm:"pk autoincr 'id'"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
FolderID int64 `xorm:"folder_id"`
|
||||
UID string `xorm:"uid"`
|
||||
Name string
|
||||
Kind int64
|
||||
Type string
|
||||
Description string
|
||||
Model json.RawMessage
|
||||
Version int64
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
|
||||
FolderName string
|
||||
FolderUID string `xorm:"folder_uid"`
|
||||
Connections int64
|
||||
CreatedBy int64
|
||||
UpdatedBy int64
|
||||
CreatedByName string
|
||||
CreatedByEmail string
|
||||
UpdatedByName string
|
||||
UpdatedByEmail string
|
||||
}
|
||||
|
||||
// LibraryElementDTO is the frontend DTO for entities.
|
||||
type LibraryElementDTO struct {
|
||||
ID int64 `json:"id"`
|
||||
OrgID int64 `json:"orgId"`
|
||||
FolderID int64 `json:"folderId"`
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Kind int64 `json:"kind"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Model json.RawMessage `json:"model"`
|
||||
Version int64 `json:"version"`
|
||||
Meta LibraryElementDTOMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// LibraryElementSearchResult is the search result for entities.
|
||||
type LibraryElementSearchResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Elements []LibraryElementDTO `json:"elements"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
||||
// LibraryElementDTOMeta is the meta information for LibraryElementDTO.
|
||||
type LibraryElementDTOMeta struct {
|
||||
FolderName string `json:"folderName"`
|
||||
FolderUID string `json:"folderUid"`
|
||||
Connections int64 `json:"connections"`
|
||||
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
|
||||
CreatedBy LibraryElementDTOMetaUser `json:"createdBy"`
|
||||
UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"`
|
||||
}
|
||||
|
||||
// LibraryElementDTOMetaUser is the meta information for user that creates/changes the library element.
|
||||
type LibraryElementDTOMetaUser struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
}
|
||||
|
||||
// libraryElementConnection is the model for library element connections.
|
||||
type libraryElementConnection struct {
|
||||
ID int64 `xorm:"pk autoincr 'id'"`
|
||||
LibraryElementID int64 `xorm:"library_element_id"`
|
||||
ConnectionKind int64 `xorm:"connection_kind"`
|
||||
ConnectionID int64 `xorm:"connection_id"`
|
||||
Created time.Time
|
||||
CreatedBy int64
|
||||
}
|
||||
|
||||
// libraryElementConnectionWithMeta is the model for library element connections with meta.
|
||||
type libraryElementConnectionWithMeta struct {
|
||||
ID int64 `xorm:"pk autoincr 'id'"`
|
||||
LibraryElementID int64 `xorm:"library_element_id"`
|
||||
ConnectionKind int64 `xorm:"connection_kind"`
|
||||
ConnectionID int64 `xorm:"connection_id"`
|
||||
Created time.Time
|
||||
CreatedBy int64
|
||||
CreatedByName string
|
||||
CreatedByEmail string
|
||||
}
|
||||
|
||||
// LibraryElementConnectionDTO is the frontend DTO for element connections.
|
||||
type LibraryElementConnectionDTO struct {
|
||||
ID int64 `json:"id"`
|
||||
Kind int64 `json:"kind"`
|
||||
ElementID int64 `json:"elementId"`
|
||||
ConnectionID int64 `json:"connectionId"`
|
||||
Created time.Time `json:"created"`
|
||||
CreatedBy LibraryElementDTOMetaUser `json:"createdBy"`
|
||||
}
|
||||
|
||||
var (
|
||||
// errLibraryElementAlreadyExists is an error for when the user tries to add a library element that already exists.
|
||||
errLibraryElementAlreadyExists = errors.New("library element with that name already exists")
|
||||
// errLibraryElementNotFound is an error for when a library element can't be found.
|
||||
errLibraryElementNotFound = errors.New("library element could not be found")
|
||||
// errLibraryElementDashboardNotFound is an error for when a library element connection can't be found.
|
||||
errLibraryElementDashboardNotFound = errors.New("library element connection could not be found")
|
||||
// errLibraryElementHasConnections is an error for when an user deletes a library element that is connected.
|
||||
errLibraryElementHasConnections = errors.New("the library element has connections")
|
||||
// errLibraryElementVersionMismatch is an error for when a library element has been changed by someone else.
|
||||
errLibraryElementVersionMismatch = errors.New("the library element has been changed by someone else")
|
||||
// errLibraryElementUnSupportedElementKind is an error for when the kind is unsupported.
|
||||
errLibraryElementUnSupportedElementKind = errors.New("the element kind is not supported")
|
||||
// ErrFolderHasConnectedLibraryElements is an error for when an user deletes a folder that contains connected library elements.
|
||||
ErrFolderHasConnectedLibraryElements = errors.New("folder contains library elements that are linked in use")
|
||||
)
|
||||
|
||||
// Commands
|
||||
|
||||
// CreateLibraryElementCommand is the command for adding a LibraryElement
|
||||
type CreateLibraryElementCommand struct {
|
||||
FolderID int64 `json:"folderId"`
|
||||
Name string `json:"name"`
|
||||
Model json.RawMessage `json:"model"`
|
||||
Kind int64 `json:"kind" binding:"Required"`
|
||||
}
|
||||
|
||||
// patchLibraryElementCommand is the command for patching a LibraryElement
|
||||
type patchLibraryElementCommand struct {
|
||||
FolderID int64 `json:"folderId" binding:"Default(-1)"`
|
||||
Name string `json:"name"`
|
||||
Model json.RawMessage `json:"model"`
|
||||
Kind int64 `json:"kind" binding:"Required"`
|
||||
Version int64 `json:"version" binding:"Required"`
|
||||
}
|
||||
|
||||
// searchLibraryElementsQuery is the query used for searching for Elements
|
||||
type searchLibraryElementsQuery struct {
|
||||
perPage int
|
||||
page int
|
||||
searchString string
|
||||
sortDirection string
|
||||
kind int
|
||||
typeFilter string
|
||||
excludeUID string
|
||||
folderFilter string
|
||||
}
|
@ -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...)
|
||||
}
|
||||
|
@ -1,144 +0,0 @@
|
||||
package librarypanels
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func (lps *LibraryPanelService) registerAPIEndpoints() {
|
||||
if !lps.IsEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
lps.RouteRegister.Group("/api/library-panels", func(libraryPanels routing.RouteRegister) {
|
||||
libraryPanels.Post("/", middleware.ReqSignedIn, binding.Bind(createLibraryPanelCommand{}), routing.Wrap(lps.createHandler))
|
||||
libraryPanels.Post("/:uid/dashboards/:dashboardId", middleware.ReqSignedIn, routing.Wrap(lps.connectHandler))
|
||||
libraryPanels.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(lps.deleteHandler))
|
||||
libraryPanels.Delete("/:uid/dashboards/:dashboardId", middleware.ReqSignedIn, routing.Wrap(lps.disconnectHandler))
|
||||
libraryPanels.Get("/", middleware.ReqSignedIn, routing.Wrap(lps.getAllHandler))
|
||||
libraryPanels.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(lps.getHandler))
|
||||
libraryPanels.Get("/:uid/dashboards/", middleware.ReqSignedIn, routing.Wrap(lps.getConnectedDashboardsHandler))
|
||||
libraryPanels.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryPanelCommand{}), routing.Wrap(lps.patchHandler))
|
||||
})
|
||||
}
|
||||
|
||||
// createHandler handles POST /api/library-panels.
|
||||
func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd createLibraryPanelCommand) response.Response {
|
||||
panel, err := lps.createLibraryPanel(c, cmd)
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to create library panel")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": panel})
|
||||
}
|
||||
|
||||
// connectHandler handles POST /api/library-panels/:uid/dashboards/:dashboardId.
|
||||
func (lps *LibraryPanelService) connectHandler(c *models.ReqContext) response.Response {
|
||||
err := lps.connectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId"))
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to connect library panel")
|
||||
}
|
||||
|
||||
return response.Success("Library panel connected")
|
||||
}
|
||||
|
||||
// deleteHandler handles DELETE /api/library-panels/:uid.
|
||||
func (lps *LibraryPanelService) deleteHandler(c *models.ReqContext) response.Response {
|
||||
err := lps.deleteLibraryPanel(c, c.Params(":uid"))
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to delete library panel")
|
||||
}
|
||||
|
||||
return response.Success("Library panel deleted")
|
||||
}
|
||||
|
||||
// disconnectHandler handles DELETE /api/library-panels/:uid/dashboards/:dashboardId.
|
||||
func (lps *LibraryPanelService) disconnectHandler(c *models.ReqContext) response.Response {
|
||||
err := lps.disconnectDashboard(c, c.Params(":uid"), c.ParamsInt64(":dashboardId"))
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to disconnect library panel")
|
||||
}
|
||||
|
||||
return response.Success("Library panel disconnected")
|
||||
}
|
||||
|
||||
// getHandler handles GET /api/library-panels/:uid.
|
||||
func (lps *LibraryPanelService) getHandler(c *models.ReqContext) response.Response {
|
||||
libraryPanel, err := lps.getLibraryPanel(c, c.Params(":uid"))
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to get library panel")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": libraryPanel})
|
||||
}
|
||||
|
||||
// getAllHandler handles GET /api/library-panels/.
|
||||
func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Response {
|
||||
query := searchLibraryPanelsQuery{
|
||||
perPage: c.QueryInt("perPage"),
|
||||
page: c.QueryInt("page"),
|
||||
searchString: c.Query("searchString"),
|
||||
sortDirection: c.Query("sortDirection"),
|
||||
panelFilter: c.Query("panelFilter"),
|
||||
excludeUID: c.Query("excludeUid"),
|
||||
folderFilter: c.Query("folderFilter"),
|
||||
}
|
||||
libraryPanels, err := lps.getAllLibraryPanels(c, query)
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to get library panels")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": libraryPanels})
|
||||
}
|
||||
|
||||
// getConnectedDashboardsHandler handles GET /api/library-panels/:uid/dashboards/.
|
||||
func (lps *LibraryPanelService) getConnectedDashboardsHandler(c *models.ReqContext) response.Response {
|
||||
dashboardIDs, err := lps.getConnectedDashboards(c, c.Params(":uid"))
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to get connected dashboards")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": dashboardIDs})
|
||||
}
|
||||
|
||||
// patchHandler handles PATCH /api/library-panels/:uid
|
||||
func (lps *LibraryPanelService) patchHandler(c *models.ReqContext, cmd patchLibraryPanelCommand) response.Response {
|
||||
libraryPanel, err := lps.patchLibraryPanel(c, cmd, c.Params(":uid"))
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to update library panel")
|
||||
}
|
||||
|
||||
return response.JSON(200, util.DynMap{"result": libraryPanel})
|
||||
}
|
||||
|
||||
func toLibraryPanelError(err error, message string) response.Response {
|
||||
if errors.Is(err, errLibraryPanelAlreadyExists) {
|
||||
return response.Error(400, errLibraryPanelAlreadyExists.Error(), err)
|
||||
}
|
||||
if errors.Is(err, errLibraryPanelNotFound) {
|
||||
return response.Error(404, errLibraryPanelNotFound.Error(), err)
|
||||
}
|
||||
if errors.Is(err, errLibraryPanelDashboardNotFound) {
|
||||
return response.Error(404, errLibraryPanelDashboardNotFound.Error(), err)
|
||||
}
|
||||
if errors.Is(err, errLibraryPanelVersionMismatch) {
|
||||
return response.Error(412, errLibraryPanelVersionMismatch.Error(), err)
|
||||
}
|
||||
if errors.Is(err, models.ErrFolderNotFound) {
|
||||
return response.Error(404, models.ErrFolderNotFound.Error(), err)
|
||||
}
|
||||
if errors.Is(err, models.ErrFolderAccessDenied) {
|
||||
return response.Error(403, models.ErrFolderAccessDenied.Error(), err)
|
||||
}
|
||||
if errors.Is(err, errLibraryPanelHasConnectedDashboards) {
|
||||
return response.Error(403, errLibraryPanelHasConnectedDashboards.Error(), err)
|
||||
}
|
||||
return response.Error(500, message, err)
|
||||
}
|
@ -1,702 +0,0 @@
|
||||
package librarypanels
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
selectLibrayPanelDTOWithMeta = `
|
||||
SELECT DISTINCT
|
||||
lp.name, lp.id, lp.org_id, lp.folder_id, lp.uid, lp.type, lp.description, lp.model, lp.created, lp.created_by, lp.updated, lp.updated_by, lp.version
|
||||
, 0 AS can_edit
|
||||
, u1.login AS created_by_name
|
||||
, u1.email AS created_by_email
|
||||
, u2.login AS updated_by_name
|
||||
, u2.email AS updated_by_email
|
||||
, (SELECT COUNT(dashboard_id) FROM library_panel_dashboard WHERE librarypanel_id = lp.id) AS connected_dashboards
|
||||
`
|
||||
fromLibrayPanelDTOWithMeta = `
|
||||
FROM library_panel AS lp
|
||||
LEFT JOIN user AS u1 ON lp.created_by = u1.id
|
||||
LEFT JOIN user AS u2 ON lp.updated_by = u2.id
|
||||
`
|
||||
sqlStatmentLibrayPanelDTOWithMeta = selectLibrayPanelDTOWithMeta + fromLibrayPanelDTOWithMeta
|
||||
)
|
||||
|
||||
func syncFieldsWithModel(libraryPanel *LibraryPanel) error {
|
||||
var model map[string]interface{}
|
||||
if err := json.Unmarshal(libraryPanel.Model, &model); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
model["title"] = libraryPanel.Name
|
||||
if model["type"] != nil {
|
||||
libraryPanel.Type = model["type"].(string)
|
||||
} else {
|
||||
model["type"] = libraryPanel.Type
|
||||
}
|
||||
if model["description"] != nil {
|
||||
libraryPanel.Description = model["description"].(string)
|
||||
} else {
|
||||
model["description"] = libraryPanel.Description
|
||||
}
|
||||
syncedModel, err := json.Marshal(&model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
libraryPanel.Model = syncedModel
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createLibraryPanel adds a Library Panel.
|
||||
func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd createLibraryPanelCommand) (LibraryPanelDTO, error) {
|
||||
libraryPanel := LibraryPanel{
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
FolderID: cmd.FolderID,
|
||||
UID: util.GenerateShortUID(),
|
||||
Name: cmd.Name,
|
||||
Model: cmd.Model,
|
||||
Version: 1,
|
||||
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
|
||||
CreatedBy: c.SignedInUser.UserId,
|
||||
UpdatedBy: c.SignedInUser.UserId,
|
||||
}
|
||||
|
||||
if err := syncFieldsWithModel(&libraryPanel); err != nil {
|
||||
return LibraryPanelDTO{}, err
|
||||
}
|
||||
|
||||
err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
if err := lps.requirePermissionsOnFolder(c.SignedInUser, cmd.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := session.Insert(&libraryPanel); err != nil {
|
||||
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
|
||||
return errLibraryPanelAlreadyExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
dto := LibraryPanelDTO{
|
||||
ID: libraryPanel.ID,
|
||||
OrgID: libraryPanel.OrgID,
|
||||
FolderID: libraryPanel.FolderID,
|
||||
UID: libraryPanel.UID,
|
||||
Name: libraryPanel.Name,
|
||||
Type: libraryPanel.Type,
|
||||
Description: libraryPanel.Description,
|
||||
Model: libraryPanel.Model,
|
||||
Version: libraryPanel.Version,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: libraryPanel.Created,
|
||||
Updated: libraryPanel.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: libraryPanel.CreatedBy,
|
||||
Name: c.SignedInUser.Login,
|
||||
AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email),
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: libraryPanel.UpdatedBy,
|
||||
Name: c.SignedInUser.Login,
|
||||
AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return dto, err
|
||||
}
|
||||
|
||||
// connectDashboard adds a connection between a Library Panel and a Dashboard.
|
||||
func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid string, dashboardID int64) error {
|
||||
err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
return lps.internalConnectDashboard(session, c.SignedInUser, uid, dashboardID)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (lps *LibraryPanelService) internalConnectDashboard(session *sqlstore.DBSession, user *models.SignedInUser,
|
||||
uid string, dashboardID int64) error {
|
||||
panel, err := getLibraryPanel(session, uid, user.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lps.requirePermissionsOnFolder(user, panel.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
libraryPanelDashboard := libraryPanelDashboard{
|
||||
DashboardID: dashboardID,
|
||||
LibraryPanelID: panel.ID,
|
||||
Created: time.Now(),
|
||||
CreatedBy: user.UserId,
|
||||
}
|
||||
if _, err := session.Insert(&libraryPanelDashboard); err != nil {
|
||||
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// connectLibraryPanelsForDashboard adds connections for all Library Panels in a Dashboard.
|
||||
func (lps *LibraryPanelService) connectLibraryPanelsForDashboard(c *models.ReqContext, uids []string, dashboardID int64) error {
|
||||
err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
_, err := session.Exec("DELETE FROM library_panel_dashboard WHERE dashboard_id=?", dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, uid := range uids {
|
||||
err := lps.internalConnectDashboard(session, c.SignedInUser, uid, dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// deleteLibraryPanel deletes a Library Panel.
|
||||
func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid string) error {
|
||||
return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lps.requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
var dashIDs []struct {
|
||||
DashboardID int64 `xorm:"dashboard_id"`
|
||||
}
|
||||
sql := "SELECT dashboard_id FROM library_panel_dashboard WHERE librarypanel_id=?"
|
||||
if err := session.SQL(sql, panel.ID).Find(&dashIDs); err != nil {
|
||||
return err
|
||||
} else if len(dashIDs) > 0 {
|
||||
return errLibraryPanelHasConnectedDashboards
|
||||
}
|
||||
|
||||
result, err := session.Exec("DELETE FROM library_panel WHERE id=?", panel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
} else if rowsAffected != 1 {
|
||||
return errLibraryPanelNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// disconnectDashboard deletes a connection between a Library Panel and a Dashboard.
|
||||
func (lps *LibraryPanelService) disconnectDashboard(c *models.ReqContext, uid string, dashboardID int64) error {
|
||||
return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lps.requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := session.Exec("DELETE FROM library_panel_dashboard WHERE librarypanel_id=? and dashboard_id=?", panel.ID, dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
} else if rowsAffected != 1 {
|
||||
return errLibraryPanelDashboardNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// disconnectLibraryPanelsForDashboard deletes connections for all Library Panels in a Dashboard.
|
||||
func (lps *LibraryPanelService) disconnectLibraryPanelsForDashboard(c *models.ReqContext, dashboardID int64, panelCount int64) error {
|
||||
return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
result, err := session.Exec("DELETE FROM library_panel_dashboard WHERE dashboard_id=?", dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
} else if rowsAffected != panelCount {
|
||||
lps.log.Warn("Number of disconnects does not match number of panels", "dashboard", dashboardID, "rowsAffected", rowsAffected, "panelCount", panelCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// deleteLibraryPanelsInFolder deletes all Library Panels for a folder.
|
||||
func (lps *LibraryPanelService) deleteLibraryPanelsInFolder(c *models.ReqContext, folderUID string) error {
|
||||
return lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
var folderUIDs []struct {
|
||||
ID int64 `xorm:"id"`
|
||||
}
|
||||
err := session.SQL("SELECT id from dashboard WHERE uid=? AND org_id=? AND is_folder=1", folderUID, c.SignedInUser.OrgId).Find(&folderUIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(folderUIDs) != 1 {
|
||||
return fmt.Errorf("found %d folders, while expecting at most one", len(folderUIDs))
|
||||
}
|
||||
folderID := folderUIDs[0].ID
|
||||
|
||||
if err := lps.requirePermissionsOnFolder(c.SignedInUser, folderID); err != nil {
|
||||
return err
|
||||
}
|
||||
var dashIDs []struct {
|
||||
DashboardID int64 `xorm:"dashboard_id"`
|
||||
}
|
||||
sql := "SELECT lpd.dashboard_id FROM library_panel AS lp"
|
||||
sql += " INNER JOIN library_panel_dashboard lpd on lp.id = lpd.librarypanel_id"
|
||||
sql += " WHERE lp.folder_id=? AND lp.org_id=?"
|
||||
err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&dashIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(dashIDs) > 0 {
|
||||
return ErrFolderHasConnectedLibraryPanels
|
||||
}
|
||||
|
||||
var panelIDs []struct {
|
||||
ID int64 `xorm:"id"`
|
||||
}
|
||||
err = session.SQL("SELECT id from library_panel WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId).Find(&panelIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, panelID := range panelIDs {
|
||||
_, err := session.Exec("DELETE FROM library_panel_dashboard WHERE librarypanel_id=?", panelID.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := session.Exec("DELETE FROM library_panel WHERE folder_id=? AND org_id=?", folderID, c.SignedInUser.OrgId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func getLibraryPanel(session *sqlstore.DBSession, uid string, orgID int64) (LibraryPanelWithMeta, error) {
|
||||
libraryPanels := make([]LibraryPanelWithMeta, 0)
|
||||
sql := sqlStatmentLibrayPanelDTOWithMeta + "WHERE lp.uid=? AND lp.org_id=?"
|
||||
sess := session.SQL(sql, uid, orgID)
|
||||
err := sess.Find(&libraryPanels)
|
||||
if err != nil {
|
||||
return LibraryPanelWithMeta{}, err
|
||||
}
|
||||
if len(libraryPanels) == 0 {
|
||||
return LibraryPanelWithMeta{}, errLibraryPanelNotFound
|
||||
}
|
||||
if len(libraryPanels) > 1 {
|
||||
return LibraryPanelWithMeta{}, fmt.Errorf("found %d panels, while expecting at most one", len(libraryPanels))
|
||||
}
|
||||
|
||||
return libraryPanels[0], nil
|
||||
}
|
||||
|
||||
// getLibraryPanel gets a Library Panel.
|
||||
func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string) (LibraryPanelDTO, error) {
|
||||
var libraryPanel LibraryPanelWithMeta
|
||||
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
libraryPanels := make([]LibraryPanelWithMeta, 0)
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
builder.Write(selectLibrayPanelDTOWithMeta)
|
||||
builder.Write(", 'General' as folder_name ")
|
||||
builder.Write(", '' as folder_uid ")
|
||||
builder.Write(fromLibrayPanelDTOWithMeta)
|
||||
builder.Write(` WHERE lp.uid=? AND lp.org_id=? AND lp.folder_id=0`, uid, c.SignedInUser.OrgId)
|
||||
builder.Write(" UNION ")
|
||||
builder.Write(selectLibrayPanelDTOWithMeta)
|
||||
builder.Write(", dashboard.title as folder_name ")
|
||||
builder.Write(", dashboard.uid as folder_uid ")
|
||||
builder.Write(fromLibrayPanelDTOWithMeta)
|
||||
builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id <> 0")
|
||||
builder.Write(` WHERE lp.uid=? AND lp.org_id=?`, uid, c.SignedInUser.OrgId)
|
||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||
}
|
||||
builder.Write(` OR dashboard.id=0`)
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(libraryPanels) == 0 {
|
||||
return errLibraryPanelNotFound
|
||||
}
|
||||
if len(libraryPanels) > 1 {
|
||||
return fmt.Errorf("found %d panels, while expecting at most one", len(libraryPanels))
|
||||
}
|
||||
|
||||
libraryPanel = libraryPanels[0]
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
dto := LibraryPanelDTO{
|
||||
ID: libraryPanel.ID,
|
||||
OrgID: libraryPanel.OrgID,
|
||||
FolderID: libraryPanel.FolderID,
|
||||
UID: libraryPanel.UID,
|
||||
Name: libraryPanel.Name,
|
||||
Type: libraryPanel.Type,
|
||||
Description: libraryPanel.Description,
|
||||
Model: libraryPanel.Model,
|
||||
Version: libraryPanel.Version,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
FolderName: libraryPanel.FolderName,
|
||||
FolderUID: libraryPanel.FolderUID,
|
||||
ConnectedDashboards: libraryPanel.ConnectedDashboards,
|
||||
Created: libraryPanel.Created,
|
||||
Updated: libraryPanel.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: libraryPanel.CreatedBy,
|
||||
Name: libraryPanel.CreatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(libraryPanel.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: libraryPanel.UpdatedBy,
|
||||
Name: libraryPanel.UpdatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(libraryPanel.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return dto, err
|
||||
}
|
||||
|
||||
// getAllLibraryPanels gets all library panels.
|
||||
func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, query searchLibraryPanelsQuery) (LibraryPanelSearchResult, error) {
|
||||
libraryPanels := make([]LibraryPanelWithMeta, 0)
|
||||
result := LibraryPanelSearchResult{}
|
||||
if query.perPage <= 0 {
|
||||
query.perPage = 100
|
||||
}
|
||||
if query.page <= 0 {
|
||||
query.page = 1
|
||||
}
|
||||
var panelFilter []string
|
||||
if len(strings.TrimSpace(query.panelFilter)) > 0 {
|
||||
panelFilter = strings.Split(query.panelFilter, ",")
|
||||
}
|
||||
folderFilter := parseFolderFilter(query)
|
||||
if folderFilter.parseError != nil {
|
||||
return LibraryPanelSearchResult{}, folderFilter.parseError
|
||||
}
|
||||
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
if folderFilter.includeGeneralFolder {
|
||||
builder.Write(selectLibrayPanelDTOWithMeta)
|
||||
builder.Write(", 'General' as folder_name ")
|
||||
builder.Write(", '' as folder_uid ")
|
||||
builder.Write(fromLibrayPanelDTOWithMeta)
|
||||
builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId)
|
||||
writeSearchStringSQL(query, lps.SQLStore, &builder)
|
||||
writeExcludeSQL(query, &builder)
|
||||
writePanelFilterSQL(panelFilter, &builder)
|
||||
builder.Write(" UNION ")
|
||||
}
|
||||
builder.Write(selectLibrayPanelDTOWithMeta)
|
||||
builder.Write(", dashboard.title as folder_name ")
|
||||
builder.Write(", dashboard.uid as folder_uid ")
|
||||
builder.Write(fromLibrayPanelDTOWithMeta)
|
||||
builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id<>0")
|
||||
builder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId)
|
||||
writeSearchStringSQL(query, lps.SQLStore, &builder)
|
||||
writeExcludeSQL(query, &builder)
|
||||
writePanelFilterSQL(panelFilter, &builder)
|
||||
if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||
}
|
||||
if query.sortDirection == search.SortAlphaDesc.Name {
|
||||
builder.Write(" ORDER BY 1 DESC")
|
||||
} else {
|
||||
builder.Write(" ORDER BY 1 ASC")
|
||||
}
|
||||
writePerPageSQL(query, lps.SQLStore, &builder)
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
retDTOs := make([]LibraryPanelDTO, 0)
|
||||
for _, panel := range libraryPanels {
|
||||
retDTOs = append(retDTOs, LibraryPanelDTO{
|
||||
ID: panel.ID,
|
||||
OrgID: panel.OrgID,
|
||||
FolderID: panel.FolderID,
|
||||
UID: panel.UID,
|
||||
Name: panel.Name,
|
||||
Type: panel.Type,
|
||||
Description: panel.Description,
|
||||
Model: panel.Model,
|
||||
Version: panel.Version,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
FolderName: panel.FolderName,
|
||||
FolderUID: panel.FolderUID,
|
||||
ConnectedDashboards: panel.ConnectedDashboards,
|
||||
Created: panel.Created,
|
||||
Updated: panel.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panel.CreatedBy,
|
||||
Name: panel.CreatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panel.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panel.UpdatedBy,
|
||||
Name: panel.UpdatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panel.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
var panels []LibraryPanel
|
||||
countBuilder := sqlstore.SQLBuilder{}
|
||||
countBuilder.Write("SELECT * FROM library_panel AS lp")
|
||||
countBuilder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId)
|
||||
writeSearchStringSQL(query, lps.SQLStore, &countBuilder)
|
||||
writeExcludeSQL(query, &countBuilder)
|
||||
writePanelFilterSQL(panelFilter, &countBuilder)
|
||||
if err := folderFilter.writeFolderFilterSQL(true, &countBuilder); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&panels); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = LibraryPanelSearchResult{
|
||||
TotalCount: int64(len(panels)),
|
||||
LibraryPanels: retDTOs,
|
||||
Page: query.page,
|
||||
PerPage: query.perPage,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// getConnectedDashboards gets all dashboards connected to a Library Panel.
|
||||
func (lps *LibraryPanelService) getConnectedDashboards(c *models.ReqContext, uid string) ([]int64, error) {
|
||||
connectedDashboardIDs := make([]int64, 0)
|
||||
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var libraryPanelDashboards []libraryPanelDashboard
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
builder.Write("SELECT lpd.* FROM library_panel_dashboard lpd")
|
||||
builder.Write(" INNER JOIN dashboard AS dashboard on lpd.dashboard_id = dashboard.id")
|
||||
builder.Write(` WHERE lpd.librarypanel_id=?`, panel.ID)
|
||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||
}
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanelDashboards); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, lpd := range libraryPanelDashboards {
|
||||
connectedDashboardIDs = append(connectedDashboardIDs, lpd.DashboardID)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return connectedDashboardIDs, err
|
||||
}
|
||||
|
||||
func (lps *LibraryPanelService) getLibraryPanelsForDashboardID(c *models.ReqContext, dashboardID int64) (map[string]LibraryPanelDTO, error) {
|
||||
libraryPanelMap := make(map[string]LibraryPanelDTO)
|
||||
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
var libraryPanels []LibraryPanelWithMeta
|
||||
sql := selectLibrayPanelDTOWithMeta + ", coalesce(dashboard.title, 'General') AS folder_name, coalesce(dashboard.uid, '') AS folder_uid " + fromLibrayPanelDTOWithMeta + `
|
||||
LEFT JOIN dashboard AS dashboard ON dashboard.id = lp.folder_id AND dashboard.id=?
|
||||
INNER JOIN library_panel_dashboard AS lpd ON lpd.librarypanel_id = lp.id AND lpd.dashboard_id=?
|
||||
`
|
||||
|
||||
sess := session.SQL(sql, dashboardID, dashboardID)
|
||||
err := sess.Find(&libraryPanels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, panel := range libraryPanels {
|
||||
libraryPanelMap[panel.UID] = LibraryPanelDTO{
|
||||
ID: panel.ID,
|
||||
OrgID: panel.OrgID,
|
||||
FolderID: panel.FolderID,
|
||||
UID: panel.UID,
|
||||
Name: panel.Name,
|
||||
Type: panel.Type,
|
||||
Description: panel.Description,
|
||||
Model: panel.Model,
|
||||
Version: panel.Version,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: panel.CanEdit,
|
||||
FolderName: panel.FolderName,
|
||||
FolderUID: panel.FolderUID,
|
||||
ConnectedDashboards: panel.ConnectedDashboards,
|
||||
Created: panel.Created,
|
||||
Updated: panel.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panel.CreatedBy,
|
||||
Name: panel.CreatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panel.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panel.UpdatedBy,
|
||||
Name: panel.UpdatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panel.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return libraryPanelMap, err
|
||||
}
|
||||
|
||||
func (lps *LibraryPanelService) handleFolderIDPatches(panelToPatch *LibraryPanel, fromFolderID int64,
|
||||
toFolderID int64, user *models.SignedInUser) error {
|
||||
// FolderID was not provided in the PATCH request
|
||||
if toFolderID == -1 {
|
||||
toFolderID = fromFolderID
|
||||
}
|
||||
|
||||
// FolderID was provided in the PATCH request
|
||||
if toFolderID != -1 && toFolderID != fromFolderID {
|
||||
if err := lps.requirePermissionsOnFolder(user, toFolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Always check permissions for the folder where library panel resides
|
||||
if err := lps.requirePermissionsOnFolder(user, fromFolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
panelToPatch.FolderID = toFolderID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// patchLibraryPanel updates a Library Panel.
|
||||
func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patchLibraryPanelCommand, uid string) (LibraryPanelDTO, error) {
|
||||
var dto LibraryPanelDTO
|
||||
err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
panelInDB, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if panelInDB.Version != cmd.Version {
|
||||
return errLibraryPanelVersionMismatch
|
||||
}
|
||||
|
||||
var libraryPanel = LibraryPanel{
|
||||
ID: panelInDB.ID,
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
FolderID: cmd.FolderID,
|
||||
UID: uid,
|
||||
Name: cmd.Name,
|
||||
Type: panelInDB.Type,
|
||||
Description: panelInDB.Description,
|
||||
Model: cmd.Model,
|
||||
Version: panelInDB.Version + 1,
|
||||
Created: panelInDB.Created,
|
||||
CreatedBy: panelInDB.CreatedBy,
|
||||
Updated: time.Now(),
|
||||
UpdatedBy: c.SignedInUser.UserId,
|
||||
}
|
||||
|
||||
if cmd.Name == "" {
|
||||
libraryPanel.Name = panelInDB.Name
|
||||
}
|
||||
if cmd.Model == nil {
|
||||
libraryPanel.Model = panelInDB.Model
|
||||
}
|
||||
if err := lps.handleFolderIDPatches(&libraryPanel, panelInDB.FolderID, cmd.FolderID, c.SignedInUser); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncFieldsWithModel(&libraryPanel); err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected, err := session.ID(panelInDB.ID).Update(&libraryPanel); err != nil {
|
||||
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
|
||||
return errLibraryPanelAlreadyExists
|
||||
}
|
||||
return err
|
||||
} else if rowsAffected != 1 {
|
||||
return errLibraryPanelNotFound
|
||||
}
|
||||
|
||||
dto = LibraryPanelDTO{
|
||||
ID: libraryPanel.ID,
|
||||
OrgID: libraryPanel.OrgID,
|
||||
FolderID: libraryPanel.FolderID,
|
||||
UID: libraryPanel.UID,
|
||||
Name: libraryPanel.Name,
|
||||
Type: libraryPanel.Type,
|
||||
Description: libraryPanel.Description,
|
||||
Model: libraryPanel.Model,
|
||||
Version: libraryPanel.Version,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: panelInDB.ConnectedDashboards,
|
||||
Created: libraryPanel.Created,
|
||||
Updated: libraryPanel.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panelInDB.CreatedBy,
|
||||
Name: panelInDB.CreatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panelInDB.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: libraryPanel.UpdatedBy,
|
||||
Name: c.SignedInUser.Login,
|
||||
AvatarUrl: dtos.GetGravatarUrl(c.SignedInUser.Email),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return dto, err
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -1,97 +0,0 @@
|
||||
package librarypanels
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConnectLibraryPanel(t *testing.T) {
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to create a connection for a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", ":dashboardId": "1"})
|
||||
resp := sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to create a connection that already exists, it should succeed",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
|
||||
resp := sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
resp = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisconnectLibraryPanel(t *testing.T) {
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to remove a connection with a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", ":dashboardId": "1"})
|
||||
resp := sc.service.disconnectHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to remove a connection that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
|
||||
resp := sc.service.disconnectHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to remove a connection that does exist, it should succeed",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
|
||||
resp := sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
resp = sc.service.disconnectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConnectedDashboards(t *testing.T) {
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get connected dashboards for a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
|
||||
resp := sc.service.getConnectedDashboardsHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get connected dashboards for a library panel that exists, but has no connections, it should return none",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.getConnectedDashboardsHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var dashResult libraryPanelDashboardsResult
|
||||
err := json.Unmarshal(resp.Body(), &dashResult)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(dashResult.Result))
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get connected dashboards for a library panel that exists and has connections, it should return connected dashboard IDs",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
firstDash := createDashboard(t, sc.sqlStore, sc.user, "Dash 1", 0)
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": strconv.FormatInt(firstDash.Id, 10)})
|
||||
resp := sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
secondDash := createDashboard(t, sc.sqlStore, sc.user, "Dash 2", 0)
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": strconv.FormatInt(secondDash.Id, 10)})
|
||||
resp = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp = sc.service.getConnectedDashboardsHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var dashResult libraryPanelDashboardsResult
|
||||
err := json.Unmarshal(resp.Body(), &dashResult)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(dashResult.Result))
|
||||
require.Equal(t, firstDash.Id, dashResult.Result[0])
|
||||
require.Equal(t, secondDash.Id, dashResult.Result[1])
|
||||
})
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package librarypanels
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestDeleteLibraryPanel(t *testing.T) {
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
resp := sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that exists, it should succeed",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel in another org, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
sc.reqContext.SignedInUser.OrgId = 2
|
||||
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
|
||||
resp := sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to delete a library panel that is connected, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
|
||||
resp := sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp = sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 403, resp.Status())
|
||||
})
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
package librarypanels
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestGetLibraryPanel(t *testing.T) {
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that exists, it should succeed and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
var expected = libraryPanelResult{
|
||||
Result: libraryPanel{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.UID,
|
||||
Name: "Text - Library Panel",
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "A description",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
FolderName: "ScenarioFolder",
|
||||
FolderUID: sc.folder.Uid,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result.Meta.Created,
|
||||
Updated: result.Result.Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get a library panel that exists in an other org, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
sc.reqContext.SignedInUser.OrgId = 2
|
||||
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get a library panel with 2 connected dashboards, it should succeed and return correct connected dashboards",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "1"})
|
||||
resp := sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": "2"})
|
||||
resp = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp = sc.service.getHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
require.Equal(t, int64(2), result.Result.Meta.ConnectedDashboards)
|
||||
})
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 }));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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 }));
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user