mirror of
https://github.com/grafana/grafana.git
synced 2026-06-09 00:23:05 -04:00
Library panel connections: Always use unistore for connection info (#121903)
This commit is contained in:
parent
dd96b0c0ba
commit
2fbbb64ce7
13 changed files with 122 additions and 797 deletions
|
|
@ -361,17 +361,7 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
|
|||
return response.Error(http.StatusBadRequest, "Use folders endpoint for deleting folders.", nil)
|
||||
}
|
||||
|
||||
// disconnect all library elements for this dashboard
|
||||
err := hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID)
|
||||
if err != nil {
|
||||
hs.log.Error(
|
||||
"Failed to disconnect library elements",
|
||||
"dashboard", dash.ID,
|
||||
"identity", c.GetID(),
|
||||
"error", err)
|
||||
}
|
||||
|
||||
err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, dash.UID, c.GetOrgID())
|
||||
err := hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, dash.UID, c.GetOrgID())
|
||||
if err != nil {
|
||||
return dashboardErrResponse(err, "Failed to delete dashboard")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,20 +359,11 @@ func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboa
|
|||
|
||||
type libraryPanelServiceMock struct {
|
||||
librarypanels.Service
|
||||
connectLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error
|
||||
importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error
|
||||
importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error
|
||||
}
|
||||
|
||||
var _ librarypanels.Service = (*libraryPanelServiceMock)(nil)
|
||||
|
||||
func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error {
|
||||
if s.connectLibraryPanelsForDashboardFunc != nil {
|
||||
return s.connectLibraryPanelsForDashboardFunc(ctx, signedInUser, dash)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *libraryPanelServiceMock) ImportLibraryPanelsForDashboard(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error {
|
||||
if s.importLibraryPanelsForDashboardFunc != nil {
|
||||
return s.importLibraryPanelsForDashboardFunc(ctx, signedInUser, libraryPanels, panels, folderID, folderUID)
|
||||
|
|
|
|||
|
|
@ -307,35 +307,26 @@ func (l *LibraryElementService) getConnectionsHandler(c *contextmodel.ReqContext
|
|||
return l.toLibraryElementError(err, "Failed to get dashboards")
|
||||
}
|
||||
|
||||
ids, err := l.getConnectionIDs(c.Req.Context(), c.SignedInUser, libraryPanelUID)
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to get connection ids")
|
||||
}
|
||||
|
||||
connections := make([]model.LibraryElementConnectionDTO, 0)
|
||||
for _, dashboard := range dashboards {
|
||||
// skip checks if the user is an admin, or if the dashboard is in the general folder
|
||||
if !c.HasRole(org.RoleAdmin) && dashboard.FolderUID != "" && dashboard.FolderUID != "general" {
|
||||
if err := l.requireViewPermissionsOnFolderUID(c.Req.Context(), c.SignedInUser, dashboard.FolderUID); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// best effort to get a connection id, once in unified storage, connections are not an individual resource and therefore do not have an id
|
||||
connectionID, ok := ids[getConnectionKey(element.ID, dashboard.ID)] // nolint:staticcheck
|
||||
if !ok {
|
||||
// if we cannot get an ID from the db, instead do a best effort to return something that will be consistent and somewhat unique for the connection.
|
||||
// note: the connection ID cannot be used to get, update, or delete a connection, so this is solely to keep the api returning the same fields for now,
|
||||
// while we deprecate the endpoint.
|
||||
hash := fnv.New64a()
|
||||
_, err := fmt.Fprintf(hash, "%d:%s:%d:%d", element.ID, dashboard.UID, c.GetOrgID(), element.Meta.Created.Unix())
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to generate connection id")
|
||||
}
|
||||
// ensure it is positive and smaller than 9007199254740991, otherwise we will lose prescision
|
||||
// in javascript, which has the safest number as 9007199254740991, compared to 9223372036854775807 in go
|
||||
connectionID = int64(hash.Sum64() & ((1 << 52) - 1))
|
||||
// connections are not an individual resource and therefore do not have an id
|
||||
// instead, return something that will be consistent and somewhat unique for the connection.
|
||||
// note: the connection ID cannot be used to get, update, or delete a connection, so this is solely to keep the api returning the same fields for now,
|
||||
// while we deprecate the endpoint.
|
||||
hash := fnv.New64a()
|
||||
_, err := fmt.Fprintf(hash, "%d:%s:%d:%d", element.ID, dashboard.UID, c.GetOrgID(), element.Meta.Created.Unix())
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to generate connection id")
|
||||
}
|
||||
// ensure it is positive and smaller than 9007199254740991, otherwise we will lose prescision
|
||||
// in javascript, which has the safest number as 9007199254740991, compared to 9223372036854775807 in go
|
||||
connectionID := int64(hash.Sum64() & ((1 << 52) - 1))
|
||||
|
||||
connections = append(connections, model.LibraryElementConnectionDTO{
|
||||
ID: connectionID,
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ SELECT DISTINCT
|
|||
, 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 ` + model.LibraryElementConnectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards`
|
||||
, u2.email AS updated_by_email`
|
||||
)
|
||||
|
||||
func getFromLibraryElementDTOWithMeta(dialect migrator.Dialect) string {
|
||||
|
|
@ -371,6 +370,10 @@ func (l *LibraryElementService) getLibraryElements(c context.Context, store db.D
|
|||
}
|
||||
}
|
||||
|
||||
if err := l.enrichConnectedDashboards(c, signedInUser.GetOrgID(), leDtos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return leDtos, nil
|
||||
}
|
||||
|
||||
|
|
@ -425,6 +428,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
|
|||
if err != nil {
|
||||
return model.LibraryElementSearchResult{}, err
|
||||
}
|
||||
retDTOs := make([]model.LibraryElementDTO, 0)
|
||||
err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
||||
builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported)
|
||||
if folderFilter.includeGeneralFolder {
|
||||
|
|
@ -462,7 +466,6 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
|
|||
}
|
||||
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc()
|
||||
retDTOs := make([]model.LibraryElementDTO, 0)
|
||||
// Get the folder tree filtered by user permissions (cached per user per request)
|
||||
folderTree, err := l.treeCache.get(c, signedInUser)
|
||||
if err != nil {
|
||||
|
|
@ -562,8 +565,15 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
|
|||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, err
|
||||
if err := l.enrichConnectedDashboards(c, signedInUser.GetOrgID(), result.Elements); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, elementToPatch *model.LibraryElement,
|
||||
|
|
@ -702,192 +712,53 @@ func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInU
|
|||
return nil
|
||||
})
|
||||
|
||||
return dto, err
|
||||
}
|
||||
|
||||
// getConnectionIDs returns a map[string]int64 with the key as elementID:connectionUID and the value as connectionID
|
||||
func (l *LibraryElementService) getConnectionIDs(c context.Context, signedInUser identity.Requester, uid string) (map[string]int64, error) {
|
||||
connections := map[string]int64{}
|
||||
recursiveQueriesAreSupported, err := l.SQLStore.RecursiveQueriesAreSupported()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return model.LibraryElementDTO{}, err
|
||||
}
|
||||
|
||||
err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
||||
var libraryElementConnections []model.LibraryElementConnectionWithMeta
|
||||
builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported)
|
||||
builder.Write("SELECT lec.id, lec.element_id, lec.connection_id")
|
||||
builder.Write(" FROM " + model.LibraryElementConnectionTableName + " AS lec ")
|
||||
builder.Write(" INNER JOIN " + model.LibraryElementTableName + " AS le ON le.id = element_id")
|
||||
builder.Write(" WHERE le.org_id=? AND le.uid=?", signedInUser.GetOrgID(), uid)
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElementConnections); err != nil {
|
||||
return err
|
||||
}
|
||||
// if the user is not an admin, we need to filter out elements that are not in folders the user can see
|
||||
for _, connection := range libraryElementConnections {
|
||||
connections[getConnectionKey(connection.ElementID, connection.ConnectionID)] = connection.ID
|
||||
}
|
||||
dtos := []model.LibraryElementDTO{dto}
|
||||
enrichErr := l.enrichConnectedDashboards(c, signedInUser.GetOrgID(), dtos)
|
||||
// do not return the error, just log it, because at this point, the patch has succeeded
|
||||
if enrichErr != nil {
|
||||
l.log.Warn("Failed to enrich connected dashboards for library element", "uid", dto.UID, "error", enrichErr)
|
||||
} else {
|
||||
dto = dtos[0]
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return connections, err
|
||||
}
|
||||
|
||||
func getConnectionKey(elementID int64, connectionID int64) string {
|
||||
return fmt.Sprintf("%d:%d", elementID, connectionID)
|
||||
}
|
||||
|
||||
// getElementsForDashboardID gets all elements for a specific dashboard
|
||||
func (l *LibraryElementService) getElementsForDashboardID(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) {
|
||||
libraryElementMap := make(map[string]model.LibraryElementDTO)
|
||||
err := l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
||||
var libraryElements []model.LibraryElementWithMeta
|
||||
sql := selectLibraryElementDTOWithMeta +
|
||||
getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()) +
|
||||
" INNER JOIN " + model.LibraryElementConnectionTableName + " AS lce ON lce.element_id = le.id AND lce.kind=1 AND lce.connection_id=?"
|
||||
sess := session.SQL(sql, dashboardID)
|
||||
err := sess.Find(&libraryElements)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc()
|
||||
for _, element := range libraryElements {
|
||||
if element.FolderName == "" {
|
||||
element.FolderName = dashboards.RootFolderName
|
||||
}
|
||||
if element.FolderUID == "" {
|
||||
element.FolderUID = ac.GeneralFolderUID
|
||||
}
|
||||
|
||||
libraryElementMap[element.UID] = model.LibraryElementDTO{
|
||||
ID: element.ID,
|
||||
OrgID: element.OrgID,
|
||||
FolderID: element.FolderID, // nolint:staticcheck
|
||||
UID: element.UID,
|
||||
Name: element.Name,
|
||||
Kind: element.Kind,
|
||||
Type: element.Type,
|
||||
Description: element.Description,
|
||||
Model: element.Model,
|
||||
Version: element.Version,
|
||||
Meta: model.LibraryElementDTOMeta{
|
||||
FolderName: element.FolderName,
|
||||
FolderUID: element.FolderUID,
|
||||
ConnectedDashboards: element.ConnectedDashboards,
|
||||
Created: element.Created,
|
||||
Updated: element.Updated,
|
||||
CreatedBy: model.LibraryElementDTOMetaUser{
|
||||
Id: element.CreatedBy,
|
||||
Name: element.CreatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(l.Cfg, element.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: model.LibraryElementDTOMetaUser{
|
||||
Id: element.UpdatedBy,
|
||||
Name: element.UpdatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(l.Cfg, element.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return libraryElementMap, err
|
||||
}
|
||||
|
||||
// connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard.
|
||||
func (l *LibraryElementService) connectElementsToDashboardID(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error {
|
||||
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
_, err := session.Exec("DELETE FROM "+model.LibraryElementConnectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, elementUID := range elementUIDs {
|
||||
element, err := l.GetLibraryElement(c, signedInUser, session, elementUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc()
|
||||
// nolint:staticcheck
|
||||
if err := l.requireViewPermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var userID int64
|
||||
if id, err := identity.UserIdentifier(signedInUser.GetID()); err == nil {
|
||||
userID = id
|
||||
}
|
||||
|
||||
connection := model.LibraryElementConnection{
|
||||
ElementID: element.ID,
|
||||
Kind: 1,
|
||||
ConnectionID: dashboardID,
|
||||
Created: time.Now(),
|
||||
CreatedBy: userID,
|
||||
}
|
||||
if _, err := session.Insert(&connection); err != nil {
|
||||
if l.SQLStore.GetDialect().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 context.Context, dashboardID int64) error {
|
||||
return l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
_, err := session.Exec("DELETE FROM "+model.LibraryElementConnectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
// deleteLibraryElementsInFolderUID deletes all Library Elements in a folder.
|
||||
func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c context.Context, signedInUser identity.Requester, folderUID string) error {
|
||||
return l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
if err := l.requireEditPermissionsOnFolderUID(c, signedInUser, folderUID); err != nil {
|
||||
if errors.Is(err, dashboards.ErrDashboardNotFound) {
|
||||
return dashboards.ErrFolderNotFound
|
||||
}
|
||||
return err
|
||||
if err := l.requireEditPermissionsOnFolderUID(c, signedInUser, folderUID); err != nil {
|
||||
if errors.Is(err, dashboards.ErrDashboardNotFound) {
|
||||
return dashboards.ErrFolderNotFound
|
||||
}
|
||||
var connectionIDs []struct {
|
||||
ConnectionID int64 `xorm:"connection_id"`
|
||||
}
|
||||
sql := "SELECT lec.connection_id FROM library_element AS le"
|
||||
sql += " INNER JOIN " + model.LibraryElementConnectionTableName + " AS lec on le.id = lec.element_id"
|
||||
sql += " WHERE le.folder_uid=? AND le.org_id=?"
|
||||
err := session.SQL(sql, folderUID, signedInUser.GetOrgID()).Find(&connectionIDs)
|
||||
return err
|
||||
}
|
||||
|
||||
var elements []struct {
|
||||
UID string `xorm:"uid"`
|
||||
}
|
||||
err := l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
||||
return session.SQL("SELECT uid FROM library_element WHERE folder_uid=? AND org_id=?", folderUID, signedInUser.GetOrgID()).Find(&elements)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceCtx, _ := identity.WithServiceIdentity(c, signedInUser.GetOrgID())
|
||||
for _, elem := range elements {
|
||||
connectedDashboards, err := l.dashboardsService.GetDashboardsByLibraryPanelUID(serviceCtx, elem.UID, signedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(connectionIDs) > 0 {
|
||||
if len(connectedDashboards) > 0 {
|
||||
return model.ErrFolderHasConnectedLibraryElements
|
||||
}
|
||||
}
|
||||
|
||||
var elementIDs []struct {
|
||||
ID int64 `xorm:"id"`
|
||||
}
|
||||
err = session.SQL("SELECT id from library_element WHERE folder_uid=? AND org_id=?", folderUID, signedInUser.GetOrgID()).Find(&elementIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, elementID := range elementIDs {
|
||||
_, err := session.Exec("DELETE FROM "+model.LibraryElementConnectionTableName+" WHERE element_id=?", elementID.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
if _, err := session.Exec("DELETE FROM library_element WHERE folder_uid=? AND org_id=?", folderUID, signedInUser.GetOrgID()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -918,3 +789,17 @@ func getFoldersWithMatchingTitles(c context.Context, l *LibraryElementService, s
|
|||
}
|
||||
return foldersWithMatchingTitles, nil
|
||||
}
|
||||
|
||||
// enrichConnectedDashboards populates the ConnectedDashboards count on each
|
||||
// DTO by querying unified search.
|
||||
func (l *LibraryElementService) enrichConnectedDashboards(ctx context.Context, orgID int64, dtos []model.LibraryElementDTO) error {
|
||||
serviceCtx, _ := identity.WithServiceIdentity(ctx, orgID)
|
||||
for i := range dtos {
|
||||
dashboards, err := l.dashboardsService.GetDashboardsByLibraryPanelUID(serviceCtx, dtos[i].UID, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dtos[i].Meta.ConnectedDashboards = int64(len(dashboards))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,18 +95,6 @@ func (l *LibraryElementService) GetElement(c context.Context, signedInUser ident
|
|||
return libraryElement, nil
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) {
|
||||
return map[string]model.LibraryElementDTO{}, nil
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package libraryelements
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
|
|
@ -51,22 +50,6 @@ func (l *LibraryElementService) requireEditPermissionsOnFolderUID(ctx context.Co
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) requireViewPermissionsOnFolder(ctx context.Context, user identity.Requester, folderID int64) error {
|
||||
evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(folderID, 10)))
|
||||
if isGeneralFolder(folderID) {
|
||||
evaluator = accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID))
|
||||
}
|
||||
canView, err := l.AccessControl.Evaluate(ctx, user, evaluator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canView {
|
||||
return dashboards.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) requireViewPermissionsOnFolderUID(ctx context.Context, user identity.Requester, folderUID string) error {
|
||||
evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID))
|
||||
canView, err := l.AccessControl.Evaluate(ctx, user, evaluator)
|
||||
|
|
|
|||
|
|
@ -47,9 +47,6 @@ type Service interface {
|
|||
PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error)
|
||||
DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error)
|
||||
GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error)
|
||||
GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error)
|
||||
ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error
|
||||
DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error
|
||||
DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error
|
||||
GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error)
|
||||
}
|
||||
|
|
@ -75,21 +72,6 @@ func (l *LibraryElementService) GetElement(c context.Context, signedInUser ident
|
|||
return l.getLibraryElementByUid(c, signedInUser, cmd, nil)
|
||||
}
|
||||
|
||||
// GetElementsForDashboard gets all connected elements for a specific dashboard.
|
||||
func (l *LibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) {
|
||||
return l.getElementsForDashboardID(c, dashboardID)
|
||||
}
|
||||
|
||||
// ConnectElementsToDashboard connects elements to a specific dashboard.
|
||||
func (l *LibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error {
|
||||
return l.connectElementsToDashboardID(c, signedInUser, elementUIDs, dashboardID)
|
||||
}
|
||||
|
||||
// DisconnectElementsFromDashboard disconnects elements from a specific dashboard.
|
||||
func (l *LibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error {
|
||||
return l.disconnectElementsFromDashboardID(c, dashboardID)
|
||||
}
|
||||
|
||||
// DeleteLibraryElementsInFolder deletes all elements for a specific folder.
|
||||
func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error {
|
||||
return l.deleteLibraryElementsInFolderUID(c, signedInUser, folderUID)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ func TestIntegration_DeleteLibraryElement(t *testing.T) {
|
|||
|
||||
scenarioWithPanel(t, "When an admin tries to delete a library panel that is connected, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.defaultGetDashByLP.Unset()
|
||||
sc.dashboardSvc.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Return([]*dashboards.DashboardRef{
|
||||
{
|
||||
UID: "dash-1",
|
||||
|
|
@ -62,23 +63,9 @@ func TestIntegration_DeleteLibraryElement(t *testing.T) {
|
|||
require.Equal(t, 403, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to delete a library panel with a stale connection table entry, it should succeed",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
// Write a stale row to library_element_connection (simulates pre-unified-storage state)
|
||||
// nolint:staticcheck
|
||||
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 9999999)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The search service says there are no real connections — deletion should succeed
|
||||
sc.dashboardSvc.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Return([]*dashboards.DashboardRef{}, nil)
|
||||
|
||||
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.deleteHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When a non-admin user cannot see a connected dashboard, deletion should still be blocked",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.defaultGetDashByLP.Unset()
|
||||
// Downgrade user to Editor, so they can delete library panels but cannot see all folders/dashboards
|
||||
sc.reqContext.OrgRole = org.RoleEditor
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
|
|
@ -129,72 +130,6 @@ func TestIntegration_GetLibraryElement(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to get a connected library panel, it should succeed and return correct connected dashboards",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := func(res libraryElementResult) libraryElementResult {
|
||||
return libraryElementResult{
|
||||
Result: libraryElement{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: 1, // nolint:staticcheck
|
||||
FolderUID: sc.folder.UID,
|
||||
UID: res.Result.UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(model.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]any{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "A description",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: model.LibraryElementDTOMeta{
|
||||
FolderName: sc.folder.Title,
|
||||
FolderUID: sc.folder.UID,
|
||||
ConnectedDashboards: 1,
|
||||
Created: res.Result.Meta.Created,
|
||||
Updated: res.Result.Meta.Updated,
|
||||
CreatedBy: model.LibraryElementDTOMetaUser{
|
||||
Id: 1,
|
||||
Name: userInDbName,
|
||||
AvatarUrl: userInDbAvatar,
|
||||
},
|
||||
UpdatedBy: model.LibraryElementDTOMetaUser{
|
||||
Id: 1,
|
||||
Name: userInDbName,
|
||||
AvatarUrl: userInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll}
|
||||
|
||||
// by uid
|
||||
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
|
||||
if diff := cmp.Diff(expected(result), result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// by name
|
||||
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":name": sc.initialResult.Result.Name})
|
||||
resp = sc.service.getByNameHandler(sc.reqContext)
|
||||
arrayResult := validateAndUnMarshalArrayResponse(t, resp)
|
||||
if diff := cmp.Diff(libraryElementArrayResult{Result: []libraryElement{expected(result).Result}}, arrayResult, 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.OrgID = 2
|
||||
|
|
@ -210,4 +145,23 @@ func TestIntegration_GetLibraryElement(t *testing.T) {
|
|||
resp = sc.service.getByNameHandler(sc.reqContext)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When a library panel has connected dashboards, connectedDashboards should reflect the count",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.defaultGetDashByLP.Unset()
|
||||
sc.dashboardSvc.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Return([]*dashboards.DashboardRef{
|
||||
{UID: "dash-1", ID: 10},
|
||||
{UID: "dash-2", ID: 20},
|
||||
}, nil)
|
||||
|
||||
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.getHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryElementResult
|
||||
err := json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), result.Result.Meta.ConnectedDashboards,
|
||||
"connectedDashboards should match the number of dashboards returned by unified search")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,12 @@ func TestIntegration_DeleteLibraryPanelsInFolder(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) {
|
||||
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
|
||||
require.NoError(t, err)
|
||||
sc.defaultGetDashByLP.Unset()
|
||||
sc.dashboardSvc.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Return([]*dashboards.DashboardRef{
|
||||
{UID: "connected-dash", ID: 1},
|
||||
}, nil)
|
||||
|
||||
err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID)
|
||||
err := sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID)
|
||||
require.EqualError(t, err, model.ErrFolderHasConnectedLibraryElements.Error())
|
||||
})
|
||||
|
||||
|
|
@ -101,18 +103,11 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
|
|||
|
||||
scenarioWithPanel(t, "When an admin tries to get connections of library panel, it should succeed and return correct result",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// add a connection where the dashboard doesn't exist. Shouldn't be returned in the list
|
||||
err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 99999999)
|
||||
require.NoError(t, err)
|
||||
|
||||
var expected = func(res model.LibraryElementConnectionsResponse) model.LibraryElementConnectionsResponse {
|
||||
return model.LibraryElementConnectionsResponse{
|
||||
Result: []model.LibraryElementConnectionDTO{
|
||||
{
|
||||
ID: sc.initialResult.Result.ID,
|
||||
ID: res.Result[0].ID, // nolint:staticcheck
|
||||
Kind: sc.initialResult.Result.Kind,
|
||||
ElementID: 1,
|
||||
ConnectionID: 1,
|
||||
|
|
@ -127,6 +122,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
sc.defaultGetDashByLP.Unset()
|
||||
sc.dashboardSvc.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Return([]*dashboards.DashboardRef{
|
||||
{
|
||||
ID: 1,
|
||||
|
|
@ -142,33 +138,6 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
|
|||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithPanel(t, "When an admin tries to create a connection with an element that exists, but the original folder does not, it should still succeed",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
b, err := json.Marshal(map[string]string{"test": "test"})
|
||||
require.NoError(t, err)
|
||||
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll}
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll}
|
||||
_, err = sc.service.CreateElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
FolderID: newFolder.ID, // nolint:staticcheck
|
||||
FolderUID: &newFolder.UID,
|
||||
Name: "Testing Library Panel With Deleted Folder",
|
||||
Kind: 1,
|
||||
Model: b,
|
||||
UID: "panel-with-deleted-folder",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = sc.service.folderService.Delete(sc.reqContext.Req.Context(), &folder.DeleteFolderCommand{
|
||||
UID: newFolder.UID,
|
||||
OrgID: sc.reqContext.OrgID,
|
||||
SignedInUser: sc.reqContext.SignedInUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
type libraryElement struct {
|
||||
|
|
@ -232,16 +201,17 @@ func getCreateCommandWithModel(folderID int64, folderUID, name string, kind mode
|
|||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
ctx *web.Context
|
||||
service *LibraryElementService
|
||||
reqContext *contextmodel.ReqContext
|
||||
user user.SignedInUser
|
||||
folder *folder.Folder
|
||||
initialResult libraryElementResult
|
||||
sqlStore db.DB
|
||||
log log.Logger
|
||||
folderSvc *foldertest.FakeService
|
||||
dashboardSvc *dashboards.FakeDashboardService
|
||||
ctx *web.Context
|
||||
service *LibraryElementService
|
||||
reqContext *contextmodel.ReqContext
|
||||
user user.SignedInUser
|
||||
folder *folder.Folder
|
||||
initialResult libraryElementResult
|
||||
sqlStore db.DB
|
||||
log log.Logger
|
||||
folderSvc *foldertest.FakeService
|
||||
dashboardSvc *dashboards.FakeDashboardService
|
||||
defaultGetDashByLP *mock.Call
|
||||
}
|
||||
|
||||
func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc *foldertest.FakeService) *folder.Folder {
|
||||
|
|
@ -388,6 +358,8 @@ func setupTestScenario(t *testing.T) scenarioContext {
|
|||
_, err = usrSvc.Create(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
defaultGetDashByLP := dashService.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Maybe().Return([]*dashboards.DashboardRef{}, nil)
|
||||
|
||||
sc := scenarioContext{
|
||||
user: usr,
|
||||
ctx: &webCtx,
|
||||
|
|
@ -397,8 +369,9 @@ func setupTestScenario(t *testing.T) scenarioContext {
|
|||
Context: &webCtx,
|
||||
SignedInUser: &usr,
|
||||
},
|
||||
folderSvc: folderSvc,
|
||||
dashboardSvc: dashService,
|
||||
folderSvc: folderSvc,
|
||||
dashboardSvc: dashService,
|
||||
defaultGetDashByLP: defaultGetDashByLP,
|
||||
}
|
||||
|
||||
sc.folder = createFolder(t, sc, "ScenarioFolder", folderSvc)
|
||||
|
|
|
|||
|
|
@ -6,12 +6,6 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
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'"`
|
||||
|
|
@ -101,29 +95,6 @@ type LibraryElementDTOMeta struct {
|
|||
UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"`
|
||||
}
|
||||
|
||||
// libraryElementConnection is the model for library element connections.
|
||||
type LibraryElementConnection struct {
|
||||
ID int64 `xorm:"pk autoincr 'id'"`
|
||||
ElementID int64 `xorm:"element_id"`
|
||||
Kind int64 `xorm:"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'"`
|
||||
ElementID int64 `xorm:"element_id"`
|
||||
Kind int64 `xorm:"kind"`
|
||||
ConnectionID int64 `xorm:"connection_id"`
|
||||
ConnectionUID string `xorm:"connection_uid"`
|
||||
Created time.Time
|
||||
CreatedBy int64
|
||||
CreatedByName string
|
||||
CreatedByEmail string
|
||||
}
|
||||
|
||||
type LibraryElementDTOMetaUser struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout
|
|||
|
||||
// Service is a service for operating on library panels.
|
||||
type Service interface {
|
||||
ConnectLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error
|
||||
ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error
|
||||
}
|
||||
|
||||
|
|
@ -62,73 +61,10 @@ type LibraryPanelService struct {
|
|||
|
||||
var _ Service = (*LibraryPanelService)(nil)
|
||||
|
||||
// ConnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard.
|
||||
func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error {
|
||||
var panels []any
|
||||
isV2 := dash.Data.Get("elements").Interface() != nil
|
||||
if isV2 {
|
||||
elementsMap := dash.Data.Get("elements").MustMap()
|
||||
panels = make([]any, 0, len(elementsMap))
|
||||
for _, element := range elementsMap {
|
||||
panels = append(panels, element)
|
||||
}
|
||||
} else {
|
||||
panels = dash.Data.Get("panels").MustArray()
|
||||
}
|
||||
libraryPanels := make(map[string]string)
|
||||
err := connectLibraryPanelsRecursively(c, panels, libraryPanels, isV2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
elementUIDs := make([]string, 0, len(libraryPanels))
|
||||
for libraryPanel := range libraryPanels {
|
||||
elementUIDs = append(elementUIDs, libraryPanel)
|
||||
}
|
||||
|
||||
return lps.LibraryElementService.ConnectElementsToDashboard(c, signedInUser, elementUIDs, dash.ID)
|
||||
}
|
||||
|
||||
func isLibraryPanelOrRow(panel *simplejson.Json, panelType string) bool {
|
||||
return panel.Interface() != nil || panelType == "row"
|
||||
}
|
||||
|
||||
func connectLibraryPanelsRecursively(c context.Context, panels []any, libraryPanels map[string]string, isV2 bool) error {
|
||||
for _, panel := range panels {
|
||||
panelAsJSON := simplejson.NewFromAny(panel)
|
||||
libraryPanel := panelAsJSON.Get("libraryPanel")
|
||||
if isV2 {
|
||||
libraryPanel = panelAsJSON.Get("spec").Get("libraryPanel")
|
||||
}
|
||||
panelType := panelAsJSON.Get("type").MustString()
|
||||
if !isLibraryPanelOrRow(libraryPanel, panelType) {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a row
|
||||
if panelType == "row" {
|
||||
rowPanels := panelAsJSON.Get("panels").MustArray()
|
||||
err := connectLibraryPanelsRecursively(c, rowPanels, libraryPanels, isV2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a library panel
|
||||
UID := libraryPanel.Get("uid").MustString()
|
||||
if len(UID) == 0 {
|
||||
return errLibraryPanelHeaderUIDMissing
|
||||
}
|
||||
_, exists := libraryPanels[UID]
|
||||
if !exists {
|
||||
libraryPanels[UID] = UID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportLibraryPanelsForDashboard loops through all panels in dashboard JSON and creates any missing library panels in the database.
|
||||
func (lps *LibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error {
|
||||
return importLibraryPanelsRecursively(c, lps.LibraryElementService, signedInUser, libraryPanels, panels, folderID, folderUID)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
|
|
@ -39,308 +40,9 @@ func TestMain(m *testing.M) {
|
|||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) {
|
||||
func TestIntegrationLibraryPanelFolderOperations(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel, it should connect the two",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
dashJSON := map[string]any{
|
||||
"panels": []any{
|
||||
map[string]any{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]any{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := dashboards.Dashboard{
|
||||
Title: "Testing ConnectLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc, &dash)
|
||||
|
||||
err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
|
||||
require.NoError(t, err)
|
||||
|
||||
elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID)
|
||||
require.NoError(t, err)
|
||||
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 V2 dashboard with a library panel, it should connect the two",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
dashJSON := map[string]any{
|
||||
"elements": []any{
|
||||
map[string]any{
|
||||
"kind": "Panel",
|
||||
"spec": map[string]any{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]any{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := dashboards.Dashboard{
|
||||
Title: "Testing ConnectLibraryPanelsForDashboard for V2 dashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc, &dash)
|
||||
|
||||
err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with library panels inside and outside of rows, it should connect all",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := model.CreateLibraryElementCommand{
|
||||
Name: "Outside row",
|
||||
Model: []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": 1,
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
"description": "A description"
|
||||
}
|
||||
`),
|
||||
Kind: int64(model.PanelElement),
|
||||
FolderUID: &sc.folder.UID,
|
||||
}
|
||||
outsidePanel, err := sc.elementService.CreateElement(sc.ctx, sc.user, cmd)
|
||||
require.NoError(t, err)
|
||||
dashJSON := map[string]any{
|
||||
"panels": []any{
|
||||
map[string]any{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"collapsed": true,
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
},
|
||||
"id": int64(2),
|
||||
"type": "row",
|
||||
"panels": []any{
|
||||
map[string]any{
|
||||
"id": int64(3),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 7,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"id": int64(4),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 13,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]any{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
},
|
||||
"title": "Inside row",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"id": int64(5),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 19,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]any{
|
||||
"uid": outsidePanel.UID,
|
||||
},
|
||||
"title": "Outside row",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := dashboards.Dashboard{
|
||||
Title: "Testing ConnectLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc, &dash)
|
||||
|
||||
err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
|
||||
require.NoError(t, err)
|
||||
|
||||
elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, elements, 2)
|
||||
require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID)
|
||||
require.Equal(t, outsidePanel.UID, elements[outsidePanel.UID].UID)
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
dashJSON := map[string]any{
|
||||
"panels": []any{
|
||||
map[string]any{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]any{
|
||||
"name": sc.initialResult.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := dashboards.Dashboard{
|
||||
Title: "Testing ConnectLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc, &dash)
|
||||
|
||||
err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, 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) {
|
||||
unused, err := sc.elementService.CreateElement(sc.ctx, sc.user, model.CreateLibraryElementCommand{
|
||||
Name: "Unused Libray Panel",
|
||||
Model: []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": 4,
|
||||
"title": "Unused Libray Panel",
|
||||
"type": "text",
|
||||
"description": "Unused description"
|
||||
}
|
||||
`),
|
||||
Kind: int64(model.PanelElement),
|
||||
FolderUID: &sc.folder.UID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
dashJSON := map[string]any{
|
||||
"panels": []any{
|
||||
map[string]any{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"id": int64(4),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]any{
|
||||
"uid": unused.UID,
|
||||
},
|
||||
"title": "Unused Libray Panel",
|
||||
"description": "Unused description",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dash := dashboards.Dashboard{
|
||||
Title: "Testing ConnectLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc, &dash)
|
||||
err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
panelJSON := []any{
|
||||
map[string]any{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]any{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]any{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
}
|
||||
dashInDB.Data.Set("panels", panelJSON)
|
||||
err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
|
||||
require.NoError(t, err)
|
||||
|
||||
elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, elements, 1)
|
||||
require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID)
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "It should return the correct count of library panels in a folder",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
count, err := sc.lps.CountInFolders(context.Background(), sc.user.OrgID, []string{sc.folder.UID}, sc.user)
|
||||
|
|
@ -731,16 +433,6 @@ func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name str
|
|||
},
|
||||
}
|
||||
}
|
||||
func createDashboard(t *testing.T, sc scenarioContext, dash *dashboards.Dashboard) *dashboards.Dashboard {
|
||||
dash.ID = 1
|
||||
dash.UID = "test-dashboard-uid"
|
||||
dash.Created = time.Now()
|
||||
dash.Updated = time.Now()
|
||||
dash.Version = 1
|
||||
|
||||
return dash
|
||||
}
|
||||
|
||||
func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -847,6 +539,8 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
|||
_, err = usrSvc.Create(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDashboardService.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Maybe().Return([]*dashboards.DashboardRef{}, nil)
|
||||
|
||||
sc := scenarioContext{
|
||||
user: usr,
|
||||
ctx: ctx,
|
||||
|
|
|
|||
Loading…
Reference in a new issue