NestedFolders: Add library panels counting and deletion to folder registry (#69149)

* Expose library element service's folder service
* Register library panels, add count implementation
* Expand folder counts test
* Update registry deletion method interface
* Allow getting library elements from any folder
* Add test for library panel deletion
* Add test for library panel counting
pull/69722/head
Arati R 2 years ago committed by GitHub
parent 32e2304f10
commit 20ffbbc41e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      pkg/api/dashboard_test.go
  2. 6
      pkg/api/folder.go
  3. 2
      pkg/services/dashboards/service/dashboard_service.go
  4. 2
      pkg/services/dashboards/service/dashboard_service_test.go
  5. 6
      pkg/services/folder/folderimpl/folder.go
  6. 324
      pkg/services/folder/folderimpl/folder_test.go
  7. 2
      pkg/services/folder/registry.go
  8. 7
      pkg/services/libraryelements/api.go
  9. 15
      pkg/services/libraryelements/database.go
  10. 6
      pkg/services/libraryelements/libraryelements.go
  11. 7
      pkg/services/libraryelements/model/model.go
  12. 41
      pkg/services/librarypanels/librarypanels.go
  13. 41
      pkg/services/librarypanels/librarypanels_test.go
  14. 2
      pkg/services/ngalert/store/alert_rule.go
  15. 2
      pkg/services/ngalert/store/alert_rule_test.go
  16. 2
      pkg/services/store/entity/models.go

@ -1359,7 +1359,7 @@ func (l *mockLibraryElementService) CreateElement(c context.Context, signedInUse
}
// GetElement gets an element from a UID.
func (l *mockLibraryElementService) GetElement(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) {
func (l *mockLibraryElementService) GetElement(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
return model.LibraryElementDTO{}, nil
}

@ -281,6 +281,12 @@ func (hs *HTTPServer) DeleteFolder(c *contextmodel.ReqContext) response.Response
}
return apierrors.ToFolderErrorResponse(err)
}
/* TODO: after a decision regarding folder deletion permissions has been made
(https://github.com/grafana/grafana-enterprise/issues/5144),
remove the previous call to hs.LibraryElementService.DeleteLibraryElementsInFolder
and remove "user" from the signature of DeleteInFolder in the folder RegistryService.
Context: https://github.com/grafana/grafana/pull/69149#discussion_r1235057903
*/
uid := web.Params(c.Req)[":uid"]
err = hs.folderService.Delete(c.Req.Context(), &folder.DeleteFolderCommand{UID: uid, OrgID: c.OrgID, ForceDeleteRules: c.QueryBool("forceDeleteRules"), SignedInUser: c.SignedInUser})

@ -635,7 +635,7 @@ func (dr DashboardServiceImpl) CountInFolder(ctx context.Context, orgID int64, f
return dr.dashboardStore.CountDashboardsInFolder(ctx, &dashboards.CountDashboardsInFolderRequest{FolderID: folder.ID, OrgID: orgID})
}
func (dr *DashboardServiceImpl) DeleteInFolder(ctx context.Context, orgID int64, folderUID string) error {
func (dr *DashboardServiceImpl) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, u *user.SignedInUser) error {
return dr.dashboardStore.DeleteDashboardsInFolder(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUID: folderUID, OrgID: orgID})
}

@ -244,7 +244,7 @@ func TestDashboardService(t *testing.T) {
t.Run("Delete dashboards in folder", func(t *testing.T) {
args := &dashboards.DeleteDashboardsInFolderRequest{OrgID: 1, FolderUID: "uid"}
fakeStore.On("DeleteDashboardsInFolder", mock.Anything, args).Return(nil).Once()
err := service.DeleteInFolder(context.Background(), 1, "uid")
err := service.DeleteInFolder(context.Background(), 1, "uid", nil)
require.NoError(t, err)
})
})

@ -509,7 +509,7 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e
}
if cmd.ForceDeleteRules {
if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID); err != nil {
if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser); err != nil {
return err
}
}
@ -525,9 +525,9 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e
return err
}
func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUID string) error {
func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) error {
for _, v := range s.registry {
if err := v.DeleteInFolder(ctx, orgID, folderUID); err != nil {
if err := v.DeleteInFolder(ctx, orgID, folderUID, user); err != nil {
return err
}
}

@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
@ -28,10 +29,14 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/ngalert/models"
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -356,7 +361,9 @@ func TestIntegrationNestedFolderService(t *testing.T) {
}
signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
orgID: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
orgID: {
dashboards.ActionFoldersCreate: {},
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
}}
createCmd := folder.CreateFolderCommand{
OrgID: orgID,
@ -364,6 +371,20 @@ func TestIntegrationNestedFolderService(t *testing.T) {
SignedInUser: &signedInUser,
}
libraryElementCmd := model.CreateLibraryElementCommand{
Model: []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`),
Kind: int64(model.PanelElement),
}
routeRegister := routing.NewRouteRegister()
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
@ -371,7 +392,12 @@ func TestIntegrationNestedFolderService(t *testing.T) {
depth := 5
t.Run("With nested folder feature flag on", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanSaveValue: true,
CanViewValue: true,
// CanEditValue is required to create library elements
CanEditValue: true,
})
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn)
require.NoError(t, err)
@ -379,6 +405,10 @@ func TestIntegrationNestedFolderService(t *testing.T) {
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, ac, dashSrv)
require.NoError(t, err)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn)
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn)
require.NoError(t, err)
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd)
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
@ -390,6 +420,13 @@ func TestIntegrationNestedFolderService(t *testing.T) {
_ = createRule(t, alertStore, parent.UID, "parent alert")
_ = createRule(t, alertStore, subfolder.UID, "sub alert")
libraryElementCmd.FolderID = parent.ID
_, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
require.NoError(t, err)
libraryElementCmd.FolderID = subfolder.ID
_, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
require.NoError(t, err)
countCmd := folder.GetDescendantCountsQuery{
UID: &ancestorUIDs[0],
OrgID: orgID,
@ -397,9 +434,10 @@ func TestIntegrationNestedFolderService(t *testing.T) {
}
m, err := serviceWithFlagOn.GetDescendantCounts(context.Background(), &countCmd)
require.NoError(t, err)
require.Equal(t, int64(depth-1), m["folder"])
require.Equal(t, int64(2), m["dashboard"])
require.Equal(t, int64(2), m["alertrule"])
require.Equal(t, int64(depth-1), m[entity.StandardKindFolder])
require.Equal(t, int64(2), m[entity.StandardKindDashboard])
require.Equal(t, int64(2), m[entity.StandardKindAlertRule])
require.Equal(t, int64(2), m[entity.StandardKindLibraryPanel])
t.Cleanup(func() {
guardian.New = origNewGuardian
@ -428,7 +466,12 @@ func TestIntegrationNestedFolderService(t *testing.T) {
}
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanSaveValue: true,
CanViewValue: true,
// CanEditValue is required to create library elements
CanEditValue: true,
})
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff,
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff)
@ -437,6 +480,10 @@ func TestIntegrationNestedFolderService(t *testing.T) {
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, ac, dashSrv)
require.NoError(t, err)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff)
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff)
require.NoError(t, err)
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd)
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
@ -448,6 +495,13 @@ func TestIntegrationNestedFolderService(t *testing.T) {
_ = createRule(t, alertStore, parent.UID, "parent alert")
_ = createRule(t, alertStore, subfolder.UID, "sub alert")
libraryElementCmd.FolderID = parent.ID
_, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
require.NoError(t, err)
libraryElementCmd.FolderID = subfolder.ID
_, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
require.NoError(t, err)
countCmd := folder.GetDescendantCountsQuery{
UID: &ancestorUIDs[0],
OrgID: orgID,
@ -455,9 +509,10 @@ func TestIntegrationNestedFolderService(t *testing.T) {
}
m, err := serviceWithFlagOff.GetDescendantCounts(context.Background(), &countCmd)
require.NoError(t, err)
require.Equal(t, int64(0), m["folder"])
require.Equal(t, int64(1), m["dashboard"])
require.Equal(t, int64(1), m["alertrule"])
require.Equal(t, int64(0), m[entity.StandardKindFolder])
require.Equal(t, int64(1), m[entity.StandardKindDashboard])
require.Equal(t, int64(1), m[entity.StandardKindAlertRule])
require.Equal(t, int64(1), m[entity.StandardKindLibraryPanel])
t.Cleanup(func() {
guardian.New = origNewGuardian
@ -470,169 +525,158 @@ func TestIntegrationNestedFolderService(t *testing.T) {
})
t.Run("Should delete folders", func(t *testing.T) {
t.Run("With nested folder feature flag on", func(t *testing.T) {
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn)
require.NoError(t, err)
featuresFlagOff := featuremgmt.WithFeatures()
serviceWithFlagOff := &Service{
cfg: cfg,
log: log.New("test-folder-service"),
dashboardFolderStore: folderStore,
features: featuresFlagOff,
bus: b,
db: db,
registry: make(map[string]folder.RegistryService),
}
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, ac, dashSrv)
require.NoError(t, err)
t.Run("With force deletion of rules", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
testCases := []struct {
service *Service
featuresFlag *featuremgmt.FeatureManager
prefix string
depth int
forceDelete bool
deletionErr error
dashboardErr error
folderErr error
libPanelParentErr error
libPanelSubErr error
desc string
}{
{
service: serviceWithFlagOn,
featuresFlag: featuresFlagOn,
prefix: "flagon-force",
depth: 3,
forceDelete: true,
dashboardErr: dashboards.ErrFolderNotFound,
folderErr: folder.ErrFolderNotFound,
libPanelParentErr: model.ErrLibraryElementNotFound,
libPanelSubErr: model.ErrLibraryElementNotFound,
desc: "With nested folder feature flag on and force deletion of rules",
},
{
service: serviceWithFlagOn,
featuresFlag: featuresFlagOn,
prefix: "flagon-noforce",
depth: 3,
forceDelete: false,
deletionErr: dashboards.ErrFolderContainsAlertRules,
desc: "With nested folder feature flag on and no force deletion of rules",
},
{
service: serviceWithFlagOff,
featuresFlag: featuresFlagOff,
prefix: "flagoff-force",
depth: 1,
forceDelete: true,
dashboardErr: dashboards.ErrFolderNotFound,
libPanelParentErr: model.ErrLibraryElementNotFound,
desc: "With nested folder feature flag off and force deletion of rules",
},
{
service: serviceWithFlagOff,
featuresFlag: featuresFlagOff,
prefix: "flagoff-noforce",
depth: 1,
forceDelete: false,
deletionErr: dashboards.ErrFolderContainsAlertRules,
desc: "With nested folder feature flag off and no force deletion of rules",
},
}
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 3, "with-force", createCmd)
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanSaveValue: true,
CanViewValue: true,
// CanEditValue is required to create library elements
CanEditValue: true,
})
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
require.NoError(t, err)
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag)
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, tc.service)
require.NoError(t, err)
_ = createRule(t, alertStore, parent.UID, "parent alert")
_ = createRule(t, alertStore, subfolder.UID, "sub alert")
deleteCmd := folder.DeleteFolderCommand{
UID: ancestorUIDs[0],
OrgID: orgID,
SignedInUser: &signedInUser,
ForceDeleteRules: true,
}
err = serviceWithFlagOn.Delete(context.Background(), &deleteCmd)
dashStore, err := database.ProvideDashboardStore(db, db.Cfg, tc.featuresFlag, tagimpl.ProvideService(db, db.Cfg), quotaService)
require.NoError(t, err)
nestedFolderStore := ProvideStore(db, db.Cfg, tc.featuresFlag)
tc.service.dashboardStore = dashStore
tc.service.store = nestedFolderStore
for i, uid := range ancestorUIDs {
// dashboard table
_, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid)
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
// folder table
_, err = serviceWithFlagOn.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID})
require.ErrorIs(t, err, folder.ErrFolderNotFound)
}
t.Cleanup(func() {
guardian.New = origNewGuardian
})
})
t.Run("Without force deletion of rules", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service)
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, ac, dashSrv)
require.NoError(t, err)
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 3, "without-force", createCmd)
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd)
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
require.NoError(t, err)
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
require.NoError(t, err)
_ = createRule(t, alertStore, parent.UID, "parent alert")
_ = createRule(t, alertStore, subfolder.UID, "sub alert")
deleteCmd := folder.DeleteFolderCommand{
UID: ancestorUIDs[0],
OrgID: orgID,
SignedInUser: &signedInUser,
ForceDeleteRules: false,
}
err = serviceWithFlagOn.Delete(context.Background(), &deleteCmd)
require.Error(t, dashboards.ErrFolderContainsAlertRules, err)
for i, uid := range ancestorUIDs {
// dashboard table
_, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid)
var (
subfolder *folder.Folder
subPanel model.LibraryElementDTO
)
if tc.depth > 1 {
subfolder, err = serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1])
require.NoError(t, err)
// folder table
_, err = serviceWithFlagOn.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID})
_ = createRule(t, alertStore, subfolder.UID, "sub alert")
libraryElementCmd.FolderID = subfolder.ID
subPanel, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
require.NoError(t, err)
}
t.Cleanup(func() {
guardian.New = origNewGuardian
})
})
})
t.Run("With nested folder feature flag off", func(t *testing.T) {
featuresFlagOff := featuremgmt.WithFeatures()
dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db, db.Cfg), quotaService)
require.NoError(t, err)
nestedFolderStore := ProvideStore(db, db.Cfg, featuresFlagOff)
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn)
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOn, ac, dashSrv)
require.NoError(t, err)
serviceWithFlagOff := &Service{
cfg: cfg,
log: log.New("test-folder-service"),
dashboardStore: dashStore,
dashboardFolderStore: folderStore,
store: nestedFolderStore,
features: featuresFlagOff,
bus: b,
db: db,
}
t.Run("With force deletion of rules", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 1, "off-force", createCmd)
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
libraryElementCmd.FolderID = parent.ID
parentPanel, err := lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
require.NoError(t, err)
_ = createRule(t, alertStore, parent.UID, "parent alert")
deleteCmd := folder.DeleteFolderCommand{
UID: ancestorUIDs[0],
OrgID: orgID,
SignedInUser: &signedInUser,
ForceDeleteRules: true,
ForceDeleteRules: tc.forceDelete,
}
err = serviceWithFlagOff.Delete(context.Background(), &deleteCmd)
require.NoError(t, err)
// dashboard table
_, err = serviceWithFlagOff.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
// folder table
_, err = serviceWithFlagOff.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[0], OrgID: orgID})
require.NoError(t, err)
t.Cleanup(func() {
guardian.New = origNewGuardian
for _, uid := range ancestorUIDs {
err := serviceWithFlagOff.store.Delete(context.Background(), uid, orgID)
require.NoError(t, err)
}
})
})
t.Run("Without force deletion of rules", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 1, "off-no-force", createCmd)
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
require.NoError(t, err)
_ = createRule(t, alertStore, parent.UID, "parent alert")
err = tc.service.Delete(context.Background(), &deleteCmd)
require.ErrorIs(t, err, tc.deletionErr)
deleteCmd := folder.DeleteFolderCommand{
UID: ancestorUIDs[0],
OrgID: orgID,
SignedInUser: &signedInUser,
ForceDeleteRules: false,
for i, uid := range ancestorUIDs {
// dashboard table
_, err := tc.service.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid)
require.ErrorIs(t, err, tc.dashboardErr)
// folder table
_, err = tc.service.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID})
require.ErrorIs(t, err, tc.folderErr)
}
err = serviceWithFlagOff.Delete(context.Background(), &deleteCmd)
require.Error(t, dashboards.ErrFolderContainsAlertRules, err)
// dashboard table
_, err = serviceWithFlagOff.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0])
require.NoError(t, err)
// folder table
_, err = serviceWithFlagOff.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[0], OrgID: orgID})
require.NoError(t, err)
_, err = lps.LibraryElementService.GetElement(context.Background(), &signedInUser, model.GetLibraryElementCommand{
FolderName: parent.Title,
FolderID: parent.ID,
UID: parentPanel.UID,
})
require.ErrorIs(t, err, tc.libPanelParentErr)
if tc.depth > 1 {
_, err = lps.LibraryElementService.GetElement(context.Background(), &signedInUser, model.GetLibraryElementCommand{
FolderName: subfolder.Title,
FolderID: subfolder.ID,
UID: subPanel.UID,
})
require.ErrorIs(t, err, tc.libPanelSubErr)
}
t.Cleanup(func() {
guardian.New = origNewGuardian
for _, uid := range ancestorUIDs {
err := serviceWithFlagOff.store.Delete(context.Background(), uid, orgID)
require.NoError(t, err)
}
})
})
})
}
})
}

@ -7,7 +7,7 @@ import (
)
type RegistryService interface {
DeleteInFolder(ctx context.Context, orgID int64, folderUID string) error
DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) error
CountInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) (int64, error)
Kind() string
}

@ -112,7 +112,12 @@ func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) respon
// 404: notFoundError
// 500: internalServerError
func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response {
element, err := l.getLibraryElementByUid(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"])
element, err := l.getLibraryElementByUid(c.Req.Context(), c.SignedInUser,
model.GetLibraryElementCommand{
UID: web.Params(c.Req)[":uid"],
FolderName: dashboards.RootFolderName,
},
)
if err != nil {
return toLibraryElementError(err, "Failed to get library element")
}

@ -228,7 +228,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn
}
// getLibraryElements gets a Library Element where param == value
func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles) ([]model.LibraryElementDTO, error) {
func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) {
libraryElements := make([]model.LibraryElementWithMeta, 0)
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
@ -239,10 +239,10 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed
err = store.WithDbSession(c, func(session *db.Session) error {
builder := db.NewSqlBuilder(cfg, features, store.GetDialect(), recursiveQueriesAreSupported)
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", 'General' as folder_name ")
builder.Write(", ? as folder_name ", cmd.FolderName)
builder.Write(", '' as folder_uid ")
builder.Write(getFromLibraryElementDTOWithMeta(store.GetDialect()))
writeParamSelectorSQL(&builder, append(params, Pair{"folder_id", 0})...)
writeParamSelectorSQL(&builder, append(params, Pair{"folder_id", cmd.FolderID})...)
builder.Write(" UNION ")
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", dashboard.title as folder_name ")
@ -303,8 +303,8 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed
}
// getLibraryElementByUid gets a Library Element by uid.
func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) {
libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: UID}}, l.features)
func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: cmd.UID}}, l.features, cmd)
if err != nil {
return model.LibraryElementDTO{}, err
}
@ -317,7 +317,10 @@ func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signed
// getLibraryElementByName gets a Library Element by name.
func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) {
return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features)
return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features,
model.GetLibraryElementCommand{
FolderName: dashboards.RootFolderName,
})
}
// getAllLibraryElements gets all Library Elements.

@ -29,7 +29,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout
// Service is a service for operating on library elements.
type Service interface {
CreateElement(c context.Context, signedInUser *user.SignedInUser, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error)
GetElement(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error)
GetElement(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error)
GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error)
ConnectElementsToDashboard(c context.Context, signedInUser *user.SignedInUser, elementUIDs []string, dashboardID int64) error
DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error
@ -52,8 +52,8 @@ func (l *LibraryElementService) CreateElement(c context.Context, signedInUser *u
}
// GetElement gets an element from a UID.
func (l *LibraryElementService) GetElement(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) {
return l.getLibraryElementByUid(c, signedInUser, UID)
func (l *LibraryElementService) GetElement(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
return l.getLibraryElementByUid(c, signedInUser, cmd)
}
// GetElementsForDashboard gets all connected elements for a specific dashboard.

@ -230,6 +230,13 @@ type PatchLibraryElementCommand struct {
UID string `json:"uid"`
}
// GetLibraryElementCommand is the command for getting a library element.
type GetLibraryElementCommand struct {
FolderName string
FolderID int64
UID string
}
// SearchLibraryElementsQuery is the query used for searching for Elements
type SearchLibraryElementsQuery struct {
PerPage int

@ -10,21 +10,30 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister,
libraryElementService libraryelements.Service) *LibraryPanelService {
return &LibraryPanelService{
libraryElementService libraryelements.Service, folderService folder.Service) (*LibraryPanelService, error) {
lps := LibraryPanelService{
Cfg: cfg,
SQLStore: sqlStore,
RouteRegister: routeRegister,
LibraryElementService: libraryElementService,
FolderService: folderService,
log: log.New("library-panels"),
}
if err := folderService.RegisterService(lps); err != nil {
return nil, err
}
return &lps, nil
}
// Service is a service for operating on library panels.
@ -44,6 +53,7 @@ type LibraryPanelService struct {
SQLStore db.DB
RouteRegister routing.RouteRegister
LibraryElementService libraryelements.Service
FolderService folder.Service
log log.Logger
}
@ -130,7 +140,7 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S
return errLibraryPanelHeaderUIDMissing
}
_, err := service.GetElement(c, signedInUser, UID)
_, err := service.GetElement(c, signedInUser, model.GetLibraryElementCommand{UID: UID, FolderName: dashboards.RootFolderName})
if err == nil {
continue
}
@ -171,3 +181,28 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S
return nil
}
// CountInFolder is a handler for retrieving the number of library panels contained
// within a given folder and for a specific organisation.
func (lps LibraryPanelService) CountInFolder(ctx context.Context, orgID int64, folderUID string, u *user.SignedInUser) (int64, error) {
var count int64
return count, lps.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
folder, err := lps.FolderService.Get(ctx, &folder.GetFolderQuery{UID: &folderUID, OrgID: orgID, SignedInUser: u})
if err != nil {
return err
}
q := sess.Table("library_element").Where("org_id = ?", u.OrgID).
Where("folder_id = ?", folder.ID).Where("kind = ?", int64(model.PanelElement))
count, err = q.Count()
return err
})
}
// DeleteInFolder deletes the library panels contained in a given folder.
func (lps LibraryPanelService) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) error {
return lps.LibraryElementService.DeleteLibraryElementsInFolder(ctx, user, folderUID)
}
// Kind returns the name of the library panel type of entity.
func (lps LibraryPanelService) Kind() string { return entity.StandardKindLibraryPanel }

@ -319,6 +319,23 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
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.CountInFolder(context.Background(), sc.user.OrgID, sc.folder.UID, sc.user)
require.NoError(t, err)
require.Equal(t, int64(1), count)
})
scenarioWithLibraryPanel(t, "It should delete library panels in a folder",
func(t *testing.T, sc scenarioContext) {
err := sc.lps.DeleteInFolder(context.Background(), sc.user.OrgID, sc.folder.UID, sc.user)
require.NoError(t, err)
_, err = sc.elementService.GetElement(sc.ctx, sc.user,
model.GetLibraryElementCommand{UID: sc.initialResult.Result.UID, FolderName: sc.folder.Title})
require.EqualError(t, err, model.ErrLibraryElementNotFound.Error())
})
}
func TestImportLibraryPanelsForDashboard(t *testing.T) {
@ -367,14 +384,16 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) {
},
}
_, err := sc.elementService.GetElement(sc.ctx, sc.user, missingUID)
_, err := sc.elementService.GetElement(sc.ctx, sc.user,
model.GetLibraryElementCommand{UID: missingUID, FolderName: dashboards.RootFolderName})
require.EqualError(t, err, model.ErrLibraryElementNotFound.Error())
err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.ctx, sc.user, missingUID)
element, err := sc.elementService.GetElement(sc.ctx, sc.user,
model.GetLibraryElementCommand{UID: missingUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
var expected = getExpected(t, element, missingUID, missingName, missingModel)
var result = toLibraryElement(t, element)
@ -406,13 +425,15 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) {
},
}
_, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID)
_, err := sc.elementService.GetElement(sc.ctx, sc.user,
model.GetLibraryElementCommand{UID: existingUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.ID)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID)
element, err := sc.elementService.GetElement(sc.ctx, sc.user,
model.GetLibraryElementCommand{UID: existingUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model)
expected.FolderID = sc.initialResult.Result.FolderID
@ -519,16 +540,15 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) {
},
},
}
_, err := sc.elementService.GetElement(sc.ctx, sc.user, outsideUID)
_, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: outsideUID, FolderName: dashboards.RootFolderName})
require.EqualError(t, err, model.ErrLibraryElementNotFound.Error())
_, err = sc.elementService.GetElement(sc.ctx, sc.user, insideUID)
_, err = sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: insideUID, FolderName: dashboards.RootFolderName})
require.EqualError(t, err, model.ErrLibraryElementNotFound.Error())
err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.ctx, sc.user, outsideUID)
element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: outsideUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
expected := getExpected(t, element, outsideUID, outsideName, outsideModel)
result := toLibraryElement(t, element)
@ -536,7 +556,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
element, err = sc.elementService.GetElement(sc.ctx, sc.user, insideUID)
element, err = sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: insideUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
expected = getExpected(t, element, insideUID, insideName, insideModel)
result = toLibraryElement(t, element)
@ -607,6 +627,7 @@ type scenarioContext struct {
folder *folder.Folder
initialResult libraryPanelResult
sqlStore db.DB
lps LibraryPanelService
}
func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement {
@ -814,6 +835,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
Cfg: cfg,
SQLStore: sqlStore,
LibraryElementService: elementService,
FolderService: folderService,
}
usr := &user.SignedInUser{
@ -853,6 +875,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
service: &service,
elementService: elementService,
sqlStore: sqlStore,
lps: service,
}
foldr := createFolder(t, sc, "ScenarioFolder")

@ -586,7 +586,7 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel
}
// DeleteInFolder deletes the rules contained in a given folder along with their associated data.
func (st DBstore) DeleteInFolder(ctx context.Context, orgID int64, folderUID string) error {
func (st DBstore) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) error {
rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{
OrgID: orgID,
NamespaceUIDs: []string{folderUID},

@ -474,7 +474,7 @@ func TestIntegration_DeleteInFolder(t *testing.T) {
}
rule := createRule(t, store, nil)
err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID)
err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, nil)
require.NoError(t, err)
c, err := store.CountInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, nil)

@ -42,7 +42,7 @@ const (
// the kind may need to change to better encapsulate { targets:[], transforms:[] }
StandardKindQuery = "query"
// KindAlertRule is not a real kind. It's used to refer to alert rules, for instance
// StandardKindAlertRule is not a real kind. It's used to refer to alert rules, for instance
// in the folder registry service.
StandardKindAlertRule = "alertrule"

Loading…
Cancel
Save