diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 07c8f29db34..b875258bfd7 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -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") } diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index 76c02f424bc..52545f3cf8c 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -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) diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 9ff8498e3c6..217e7d4a351 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -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, diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 44b3f770c15..93802589772 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -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 +} diff --git a/pkg/services/libraryelements/fake/libraryelements_service.go b/pkg/services/libraryelements/fake/libraryelements_service.go index 13f7aef322d..0ddfda19ac9 100644 --- a/pkg/services/libraryelements/fake/libraryelements_service.go +++ b/pkg/services/libraryelements/fake/libraryelements_service.go @@ -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 } diff --git a/pkg/services/libraryelements/guard.go b/pkg/services/libraryelements/guard.go index f372fe641ac..0d7fcacdc75 100644 --- a/pkg/services/libraryelements/guard.go +++ b/pkg/services/libraryelements/guard.go @@ -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) diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index d6dd0cf5903..b0663caa60a 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -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) diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go index b6a7333eb93..9effec0a906 100644 --- a/pkg/services/libraryelements/libraryelements_delete_test.go +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -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 diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index 1d9912195dd..cdb9eb63398 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -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") + }) } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 5359f67c9ad..f7ca3afc45f 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -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) diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index 4868bf50cbf..8ae51a256c8 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -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"` diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index ca08b57dc0d..58e7722412f 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -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) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 6c5bdb6ec5f..d4fd1af5408 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -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,