Folders: Reduce DB queries when counting and deleting resources under folders (#81153)

* Add folder store method for fetching all folder descendants

* Modify GetDescendantCounts() to fetch folder descendants at once

* Reduce DB calls when counting library panels under dashboard

* Reduce DB calls when counting dashboards under folder

* Reduce DB calls during folder delete

* Modify folder registry to count/delete entities under multiple folders

* Reduce DB calls when counting

* Reduce DB calls when deleting
This commit is contained in:
Sofia Papagiannaki 2024-01-30 18:26:34 +02:00 committed by GitHub
parent 0139ac205d
commit 89d3b55bec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 335 additions and 411 deletions

View File

@ -24,7 +24,7 @@ type DashboardService interface {
ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error)
SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error)
SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) (model.HitList, error)
CountInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) (int64, error)
CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error)
GetDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*Dashboard, error)
}
@ -74,6 +74,6 @@ type Store interface {
Count(context.Context, *quota.ScopeParameters) (*quota.Map, error)
// CountDashboardsInFolder returns the number of dashboards associated with
// the given parent folder ID.
CountDashboardsInFolder(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error)
DeleteDashboardsInFolder(ctx context.Context, request *DeleteDashboardsInFolderRequest) error
CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error)
DeleteDashboardsInFolders(ctx context.Context, request *DeleteDashboardsInFolderRequest) error
}

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.38.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package dashboards
@ -18,10 +18,6 @@ type FakeDashboardProvisioning struct {
func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error {
ret := _m.Called(ctx, cmd)
if len(ret) == 0 {
panic("no return value specified for DeleteOrphanedProvisionedDashboards")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *DeleteOrphanedProvisionedDashboardsCommand) error); ok {
r0 = rf(ctx, cmd)
@ -36,10 +32,6 @@ func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx con
func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error {
ret := _m.Called(ctx, dashboardID, orgID)
if len(ret) == 0 {
panic("no return value specified for DeleteProvisionedDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok {
r0 = rf(ctx, dashboardID, orgID)
@ -54,10 +46,6 @@ func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Cont
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) {
ret := _m.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDashboardData")
}
var r0 []*DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*DashboardProvisioning, error)); ok {
@ -84,10 +72,6 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(ctx context.Con
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) {
ret := _m.Called(ctx, dashboardID)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDashboardDataByDashboardID")
}
var r0 *DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (*DashboardProvisioning, error)); ok {
@ -114,10 +98,6 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(ct
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error) {
ret := _m.Called(ctx, orgID, dashboardUID)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDashboardDataByDashboardUID")
}
var r0 *DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*DashboardProvisioning, error)); ok {
@ -144,10 +124,6 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(c
func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *folder.CreateFolderCommand) (*folder.Folder, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for SaveFolderForProvisionedDashboards")
}
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *folder.CreateFolderCommand) (*folder.Folder, error)); ok {
@ -174,10 +150,6 @@ func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 cont
func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error) {
ret := _m.Called(ctx, dto, provisioning)
if len(ret) == 0 {
panic("no return value specified for SaveProvisionedDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *DashboardProvisioning) (*Dashboard, error)); ok {
@ -204,10 +176,6 @@ func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Contex
func (_m *FakeDashboardProvisioning) UnprovisionDashboard(ctx context.Context, dashboardID int64) error {
ret := _m.Called(ctx, dashboardID)
if len(ret) == 0 {
panic("no return value specified for UnprovisionDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, dashboardID)

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.38.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package dashboards
@ -20,10 +20,6 @@ type FakeDashboardService struct {
func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*SaveDashboardCommand, error) {
ret := _m.Called(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard)
if len(ret) == 0 {
panic("no return value specified for BuildSaveDashboardCommand")
}
var r0 *SaveDashboardCommand
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool, bool) (*SaveDashboardCommand, error)); ok {
@ -46,27 +42,23 @@ func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, d
return r0, r1
}
// CountInFolder provides a mock function with given fields: ctx, orgID, folderUID, user
func (_m *FakeDashboardService) CountInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) (int64, error) {
ret := _m.Called(ctx, orgID, folderUID, user)
if len(ret) == 0 {
panic("no return value specified for CountInFolder")
}
// CountInFolders provides a mock function with given fields: ctx, orgID, folderUIDs, user
func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) {
ret := _m.Called(ctx, orgID, folderUIDs, user)
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string, identity.Requester) (int64, error)); ok {
return rf(ctx, orgID, folderUID, user)
if rf, ok := ret.Get(0).(func(context.Context, int64, []string, identity.Requester) (int64, error)); ok {
return rf(ctx, orgID, folderUIDs, user)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string, identity.Requester) int64); ok {
r0 = rf(ctx, orgID, folderUID, user)
if rf, ok := ret.Get(0).(func(context.Context, int64, []string, identity.Requester) int64); ok {
r0 = rf(ctx, orgID, folderUIDs, user)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, int64, string, identity.Requester) error); ok {
r1 = rf(ctx, orgID, folderUID, user)
if rf, ok := ret.Get(1).(func(context.Context, int64, []string, identity.Requester) error); ok {
r1 = rf(ctx, orgID, folderUIDs, user)
} else {
r1 = ret.Error(1)
}
@ -78,10 +70,6 @@ func (_m *FakeDashboardService) CountInFolder(ctx context.Context, orgID int64,
func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error {
ret := _m.Called(ctx, dashboardId, orgId)
if len(ret) == 0 {
panic("no return value specified for DeleteDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok {
r0 = rf(ctx, dashboardId, orgId)
@ -96,10 +84,6 @@ func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId
func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for FindDashboards")
}
var r0 []DashboardSearchProjection
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error)); ok {
@ -126,10 +110,6 @@ func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindP
func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) (*Dashboard, error)); ok {
@ -156,10 +136,6 @@ func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDash
func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboardTags")
}
var r0 []*DashboardTagCloudItem
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error)); ok {
@ -186,10 +162,6 @@ func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *Get
func (_m *FakeDashboardService) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboardUIDByID")
}
var r0 *DashboardRef
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) (*DashboardRef, error)); ok {
@ -216,10 +188,6 @@ func (_m *FakeDashboardService) GetDashboardUIDByID(ctx context.Context, query *
func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboards")
}
var r0 []*Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) ([]*Dashboard, error)); ok {
@ -246,10 +214,6 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDas
func (_m *FakeDashboardService) GetDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*Dashboard, error) {
ret := _m.Called(ctx, user)
if len(ret) == 0 {
panic("no return value specified for GetDashboardsSharedWithUser")
}
var r0 []*Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) ([]*Dashboard, error)); ok {
@ -276,10 +240,6 @@ func (_m *FakeDashboardService) GetDashboardsSharedWithUser(ctx context.Context,
func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) {
ret := _m.Called(ctx, dto)
if len(ret) == 0 {
panic("no return value specified for ImportDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) (*Dashboard, error)); ok {
@ -306,10 +266,6 @@ func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDa
func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) {
ret := _m.Called(ctx, dto, allowUiUpdate)
if len(ret) == 0 {
panic("no return value specified for SaveDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool) (*Dashboard, error)); ok {
@ -336,10 +292,6 @@ func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDash
func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) (model.HitList, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for SearchDashboards")
}
var r0 model.HitList
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) (model.HitList, error)); ok {

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"
"xorm.io/xorm"
@ -657,7 +658,12 @@ func (d *dashboardStore) DeleteDashboard(ctx context.Context, cmd *dashboards.De
}
func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, sess *db.Session, emitEntityEvent bool) error {
dashboard := dashboards.Dashboard{ID: cmd.ID, OrgID: cmd.OrgID}
dashboard := dashboards.Dashboard{OrgID: cmd.OrgID}
if cmd.UID != "" {
dashboard.UID = cmd.UID
} else {
dashboard.ID = cmd.ID
}
has, err := sess.Get(&dashboard)
if err != nil {
return err
@ -1008,29 +1014,41 @@ func (d *dashboardStore) GetDashboardTags(ctx context.Context, query *dashboards
// CountDashboardsInFolder returns a count of all dashboards associated with the
// given parent folder ID.
//
// This will be updated to take CountDashboardsInFolderQuery as an argument and
// lookup dashboards using the ParentFolderUID when dashboards are associated with a parent folder UID instead of ID.
func (d *dashboardStore) CountDashboardsInFolder(
func (d *dashboardStore) CountDashboardsInFolders(
ctx context.Context, req *dashboards.CountDashboardsInFolderRequest) (int64, error) {
if len(req.FolderUIDs) == 0 {
return 0, nil
}
var count int64
var err error
err = d.store.WithDbSession(ctx, func(sess *db.Session) error {
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
// nolint:staticcheck
session := sess.In("folder_id", req.FolderID).In("org_id", req.OrgID).
In("is_folder", d.store.GetDialect().BooleanStr(false))
count, err = session.Count(&dashboards.Dashboard{})
s := strings.Builder{}
args := make([]any, 0, 3)
s.WriteString("SELECT COUNT(*) FROM dashboard WHERE ")
if len(req.FolderUIDs) == 1 && req.FolderUIDs[0] == "" {
s.WriteString("folder_uid IS NULL")
} else {
s.WriteString(fmt.Sprintf("folder_uid IN (%s)", strings.Repeat("?,", len(req.FolderUIDs)-1)+"?"))
for _, folderUID := range req.FolderUIDs {
args = append(args, folderUID)
}
}
s.WriteString(" AND org_id = ? AND is_folder = ?")
args = append(args, req.OrgID, d.store.GetDialect().BooleanStr(false))
sql := s.String()
_, err := sess.SQL(sql, args...).Get(&count)
return err
})
return count, err
}
func (d *dashboardStore) DeleteDashboardsInFolder(
func (d *dashboardStore) DeleteDashboardsInFolders(
ctx context.Context, req *dashboards.DeleteDashboardsInFolderRequest) error {
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
// TODO delete all dashboards in the folder in a bulk query
for _, folderUID := range req.FolderUIDs {
dashboard := dashboards.Dashboard{OrgID: req.OrgID}
has, err := sess.Where("uid = ? AND org_id = ?", req.FolderUID, req.OrgID).Get(&dashboard)
has, err := sess.Where("org_id = ? AND uid = ?", req.OrgID, folderUID).Get(&dashboard)
if err != nil {
return err
}
@ -1043,7 +1061,11 @@ func (d *dashboardStore) DeleteDashboardsInFolder(
}
_, err = sess.Where("folder_id = ? AND org_id = ? AND is_folder = ?", dashboard.ID, dashboard.OrgID, false).Delete(&dashboards.Dashboard{})
if err != nil {
return err
}
}
return nil
})
}

View File

@ -47,6 +47,11 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService)
require.NoError(t, err)
// insertTestDashboard creates the following hierarchy:
// 1 test dash folder
// test dash 23
// test dash 45
// test dash 67
savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp")
savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp")
insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, savedFolder.UID, false, "prod")
@ -470,17 +475,15 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
t.Run("Can count dashboards by parent folder", func(t *testing.T) {
setup()
// setup() saves one dashboard in the general folder and two in the "savedFolder".
count, err := dashboardStore.CountDashboardsInFolder(
count, err := dashboardStore.CountDashboardsInFolders(
context.Background(),
// nolint:staticcheck
&dashboards.CountDashboardsInFolderRequest{FolderID: 0, OrgID: 1})
&dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{""}, OrgID: 1})
require.NoError(t, err)
require.Equal(t, int64(1), count)
count, err = dashboardStore.CountDashboardsInFolder(
count, err = dashboardStore.CountDashboardsInFolders(
context.Background(),
// nolint:staticcheck
&dashboards.CountDashboardsInFolderRequest{FolderID: savedFolder.ID, OrgID: 1})
&dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
require.NoError(t, err)
require.Equal(t, int64(2), count)
})
@ -491,16 +494,16 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
_ = insertTestDashboard(t, dashboardStore, "delete me 1", 1, folder.ID, folder.UID, false, "delete this 1")
_ = insertTestDashboard(t, dashboardStore, "delete me 2", 1, folder.ID, folder.UID, false, "delete this 2")
err := dashboardStore.DeleteDashboardsInFolder(
err := dashboardStore.DeleteDashboardsInFolders(
context.Background(),
&dashboards.DeleteDashboardsInFolderRequest{
FolderUID: folder.UID,
FolderUIDs: []string{folder.UID},
OrgID: 1,
})
require.NoError(t, err)
// nolint:staticcheck
count, err := dashboardStore.CountDashboardsInFolder(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderID: 2, OrgID: 1})
count, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{folder.UID}, OrgID: 1})
require.NoError(t, err)
require.Equal(t, count, int64(0))
})

View File

@ -220,6 +220,7 @@ type DashboardProvisioning struct {
type DeleteDashboardCommand struct {
ID int64
UID string
OrgID int64
ForceDeleteFolderRules bool
}
@ -322,8 +323,7 @@ type CountDashboardsInFolderQuery struct {
// to the store layer. The FolderID will be replaced with FolderUID when
// dashboards are updated with parent folder UIDs.
type CountDashboardsInFolderRequest struct {
// Deprecated: use FolderUID instead
FolderID int64
FolderUIDs []string
OrgID int64
}
@ -345,7 +345,7 @@ func FromDashboard(dash *Dashboard) *folder.Folder {
}
type DeleteDashboardsInFolderRequest struct {
FolderUID string
FolderUIDs []string
OrgID int64
}

View File

@ -733,19 +733,12 @@ func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *das
return dr.dashboardStore.GetDashboardTags(ctx, query)
}
func (dr DashboardServiceImpl) CountInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) (int64, error) {
folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{UID: &folderUID, OrgID: orgID, SignedInUser: u})
if err != nil {
return 0, err
}
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
// nolint:staticcheck
return dr.dashboardStore.CountDashboardsInFolder(ctx, &dashboards.CountDashboardsInFolderRequest{FolderID: folder.ID, OrgID: orgID})
func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) {
return dr.dashboardStore.CountDashboardsInFolders(ctx, &dashboards.CountDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID})
}
func (dr *DashboardServiceImpl) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) error {
return dr.dashboardStore.DeleteDashboardsInFolder(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUID: folderUID, OrgID: orgID})
func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) error {
return dr.dashboardStore.DeleteDashboardsInFolders(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID})
}
func (dr *DashboardServiceImpl) Kind() string { return entity.StandardKindDashboard }

View File

@ -225,21 +225,21 @@ func TestDashboardService(t *testing.T) {
})
t.Run("Count dashboards in folder", func(t *testing.T) {
fakeStore.On("CountDashboardsInFolder", mock.Anything, mock.AnythingOfType("*dashboards.CountDashboardsInFolderRequest")).Return(int64(3), nil)
fakeStore.On("CountDashboardsInFolders", mock.Anything, mock.AnythingOfType("*dashboards.CountDashboardsInFolderRequest")).Return(int64(3), nil)
folderSvc.ExpectedFolder = &folder.Folder{UID: "i am a folder"}
// set up a ctx with signed in user
usr := &user.SignedInUser{UserID: 1}
ctx := appcontext.WithUser(context.Background(), usr)
count, err := service.CountInFolder(ctx, 1, "i am a folder", usr)
count, err := service.CountInFolders(ctx, 1, []string{"i am a folder"}, usr)
require.NoError(t, err)
require.Equal(t, int64(3), count)
})
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", nil)
args := &dashboards.DeleteDashboardsInFolderRequest{OrgID: 1, FolderUIDs: []string{"uid"}}
fakeStore.On("DeleteDashboardsInFolders", mock.Anything, args).Return(nil).Once()
err := service.DeleteInFolders(context.Background(), 1, []string{"uid"}, nil)
require.NoError(t, err)
})
})

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.38.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package dashboards
@ -20,10 +20,6 @@ type FakeDashboardStore struct {
func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParameters) (*quota.Map, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Count")
}
var r0 *quota.Map
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *quota.ScopeParameters) (*quota.Map, error)); ok {
@ -46,14 +42,10 @@ func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParamet
return r0, r1
}
// CountDashboardsInFolder provides a mock function with given fields: ctx, request
func (_m *FakeDashboardStore) CountDashboardsInFolder(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) {
// CountDashboardsInFolders provides a mock function with given fields: ctx, request
func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) {
ret := _m.Called(ctx, request)
if len(ret) == 0 {
panic("no return value specified for CountDashboardsInFolder")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *CountDashboardsInFolderRequest) (int64, error)); ok {
@ -78,10 +70,6 @@ func (_m *FakeDashboardStore) CountDashboardsInFolder(ctx context.Context, reque
func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error {
ret := _m.Called(ctx, cmd)
if len(ret) == 0 {
panic("no return value specified for DeleteDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *DeleteDashboardCommand) error); ok {
r0 = rf(ctx, cmd)
@ -92,14 +80,10 @@ func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDa
return r0
}
// DeleteDashboardsInFolder provides a mock function with given fields: ctx, request
func (_m *FakeDashboardStore) DeleteDashboardsInFolder(ctx context.Context, request *DeleteDashboardsInFolderRequest) error {
// DeleteDashboardsInFolders provides a mock function with given fields: ctx, request
func (_m *FakeDashboardStore) DeleteDashboardsInFolders(ctx context.Context, request *DeleteDashboardsInFolderRequest) error {
ret := _m.Called(ctx, request)
if len(ret) == 0 {
panic("no return value specified for DeleteDashboardsInFolder")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *DeleteDashboardsInFolderRequest) error); ok {
r0 = rf(ctx, request)
@ -114,10 +98,6 @@ func (_m *FakeDashboardStore) DeleteDashboardsInFolder(ctx context.Context, requ
func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error {
ret := _m.Called(ctx, cmd)
if len(ret) == 0 {
panic("no return value specified for DeleteOrphanedProvisionedDashboards")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *DeleteOrphanedProvisionedDashboardsCommand) error); ok {
r0 = rf(ctx, cmd)
@ -132,10 +112,6 @@ func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Co
func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for FindDashboards")
}
var r0 []DashboardSearchProjection
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error)); ok {
@ -162,10 +138,6 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPer
func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) (*Dashboard, error)); ok {
@ -192,10 +164,6 @@ func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *GetDashbo
func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboardTags")
}
var r0 []*DashboardTagCloudItem
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error)); ok {
@ -222,10 +190,6 @@ func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDa
func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboardUIDByID")
}
var r0 *DashboardRef
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) (*DashboardRef, error)); ok {
@ -252,10 +216,6 @@ func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *Ge
func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboards")
}
var r0 []*Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) ([]*Dashboard, error)); ok {
@ -282,10 +242,6 @@ func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashb
func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboardsByPluginID")
}
var r0 []*Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsByPluginIDQuery) ([]*Dashboard, error)); ok {
@ -312,10 +268,6 @@ func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query
func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) {
ret := _m.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDashboardData")
}
var r0 []*DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*DashboardProvisioning, error)); ok {
@ -342,10 +294,6 @@ func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, n
func (_m *FakeDashboardStore) GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) {
ret := _m.Called(ctx, dashboardID)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDataByDashboardID")
}
var r0 *DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (*DashboardProvisioning, error)); ok {
@ -372,10 +320,6 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardID(ctx context.Contex
func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error) {
ret := _m.Called(ctx, orgID, dashboardUID)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDataByDashboardUID")
}
var r0 *DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*DashboardProvisioning, error)); ok {
@ -402,10 +346,6 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(ctx context.Conte
func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error {
ret := _m.Called(ctx, dashID, alerts)
if len(ret) == 0 {
panic("no return value specified for SaveAlerts")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, []*models.Alert) error); ok {
r0 = rf(ctx, dashID, alerts)
@ -420,10 +360,6 @@ func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, aler
func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) {
ret := _m.Called(ctx, cmd)
if len(ret) == 0 {
panic("no return value specified for SaveDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, SaveDashboardCommand) (*Dashboard, error)); ok {
@ -450,10 +386,6 @@ func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd SaveDashboa
func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) {
ret := _m.Called(ctx, cmd, provisioning)
if len(ret) == 0 {
panic("no return value specified for SaveProvisionedDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, SaveDashboardCommand, *DashboardProvisioning) (*Dashboard, error)); ok {
@ -480,10 +412,6 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd
func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for UnprovisionDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, id)
@ -498,10 +426,6 @@ func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64
func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) {
ret := _m.Called(ctx, dashboard, overwrite)
if len(ret) == 0 {
panic("no return value specified for ValidateDashboardBeforeSave")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *Dashboard, bool) (bool, error)); ok {

View File

@ -748,29 +748,18 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e
return dashboards.ErrFolderAccessDenied
}
result := []string{cmd.UID}
folders := []string{cmd.UID}
err = s.db.InTransaction(ctx, func(ctx context.Context) error {
subfolders, err := s.nestedFolderDelete(ctx, cmd)
descendants, err := s.nestedFolderDelete(ctx, cmd)
if err != nil {
logger.Error("the delete folder on folder table failed with err: ", "error", err)
return err
}
result = append(result, subfolders...)
dashFolders, err := s.dashboardFolderStore.GetFolders(ctx, cmd.OrgID, result)
if err != nil {
return folder.ErrInternal.Errorf("failed to fetch subfolders from dashboard store: %w", err)
}
for _, foldr := range result {
dashFolder, ok := dashFolders[foldr]
if !ok {
return folder.ErrInternal.Errorf("folder does not exist in dashboard store")
}
folders = append(folders, descendants...)
if cmd.ForceDeleteRules {
if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser); err != nil {
if err := s.deleteChildrenInFolder(ctx, cmd.OrgID, folders, cmd.SignedInUser); err != nil {
return err
}
} else {
@ -778,7 +767,7 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e
if !ok {
return folder.ErrInternal.Errorf("no alert rule service found in registry")
}
alertRulesInFolder, err := alertRuleSrv.CountInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser)
alertRulesInFolder, err := alertRuleSrv.CountInFolders(ctx, cmd.OrgID, folders, cmd.SignedInUser)
if err != nil {
s.log.Error("failed to count alert rules in folder", "error", err)
return err
@ -788,33 +777,34 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e
}
}
if err = s.legacyDelete(ctx, cmd, dashFolder); err != nil {
if err = s.legacyDelete(ctx, cmd, folders); err != nil {
return err
}
}
return nil
})
return err
}
func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) error {
func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) error {
for _, v := range s.registry {
if err := v.DeleteInFolder(ctx, orgID, folderUID, user); err != nil {
if err := v.DeleteInFolders(ctx, orgID, folderUIDs, user); err != nil {
return err
}
}
return nil
}
func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderCommand, dashFolder *folder.Folder) error {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
// nolint:staticcheck
deleteCmd := dashboards.DeleteDashboardCommand{OrgID: cmd.OrgID, ID: dashFolder.ID, ForceDeleteFolderRules: cmd.ForceDeleteRules}
func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderCommand, folderUIDs []string) error {
// TODO use bulk delete
for _, folderUID := range folderUIDs {
deleteCmd := dashboards.DeleteDashboardCommand{OrgID: cmd.OrgID, UID: folderUID, ForceDeleteFolderRules: cmd.ForceDeleteRules}
if err := s.dashboardStore.DeleteDashboard(ctx, &deleteCmd); err != nil {
return toFolderError(err)
}
}
return nil
}
@ -907,9 +897,9 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol
// the folder store and returns the UIDs for all its descendants.
func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFolderCommand) ([]string, error) {
logger := s.log.FromContext(ctx)
result := []string{}
descendantUIDs := []string{}
if cmd.SignedInUser == nil {
return result, folder.ErrBadRequest.Errorf("missing signed in user")
return descendantUIDs, folder.ErrBadRequest.Errorf("missing signed in user")
}
_, err := s.Get(ctx, &folder.GetFolderQuery{
@ -918,31 +908,26 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold
SignedInUser: cmd.SignedInUser,
})
if err != nil {
return result, err
return descendantUIDs, err
}
folders, err := s.store.GetChildren(ctx, folder.GetChildrenQuery{UID: cmd.UID, OrgID: cmd.OrgID})
descendants, err := s.store.GetDescendants(ctx, cmd.OrgID, cmd.UID)
if err != nil {
return result, err
}
for _, f := range folders {
result = append(result, f.UID)
logger.Info("deleting subfolder", "org_id", f.OrgID, "uid", f.UID)
subfolders, err := s.nestedFolderDelete(ctx, &folder.DeleteFolderCommand{UID: f.UID, OrgID: f.OrgID, ForceDeleteRules: cmd.ForceDeleteRules, SignedInUser: cmd.SignedInUser})
if err != nil {
logger.Error("failed deleting subfolder", "org_id", f.OrgID, "uid", f.UID, "error", err)
return result, err
}
result = append(result, subfolders...)
logger.Error("failed to get descendant folders", "error", err)
return descendantUIDs, err
}
logger.Info("deleting folder and its contents", "org_id", cmd.OrgID, "uid", cmd.UID)
err = s.store.Delete(ctx, cmd.UID, cmd.OrgID)
for _, f := range descendants {
descendantUIDs = append(descendantUIDs, f.UID)
}
logger.Info("deleting folder and its descendants", "org_id", cmd.OrgID, "uid", cmd.UID)
toDelete := append(descendantUIDs, cmd.UID)
err = s.store.Delete(ctx, toDelete, cmd.OrgID)
if err != nil {
logger.Info("failed deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID, "err", err)
return result, err
return descendantUIDs, err
}
return result, nil
return descendantUIDs, nil
}
func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
@ -950,56 +935,38 @@ func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescenda
if q.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed-in user")
}
if *q.UID == "" {
if q.UID == nil || *q.UID == "" {
return nil, folder.ErrBadRequest.Errorf("missing UID")
}
if q.OrgID < 1 {
return nil, folder.ErrBadRequest.Errorf("invalid orgID")
}
result := []string{*q.UID}
folders := []string{*q.UID}
countsMap := make(folder.DescendantCounts, len(s.registry)+1)
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
subfolders, err := s.getNestedFolders(ctx, q.OrgID, *q.UID)
descendantFolders, err := s.store.GetDescendants(ctx, q.OrgID, *q.UID)
if err != nil {
logger.Error("failed to get subfolders", "error", err)
logger.Error("failed to get descendant folders", "error", err)
return nil, err
}
result = append(result, subfolders...)
countsMap[entity.StandardKindFolder] = int64(len(subfolders))
for _, f := range descendantFolders {
folders = append(folders, f.UID)
}
countsMap[entity.StandardKindFolder] = int64(len(descendantFolders))
}
for _, v := range s.registry {
for _, folder := range result {
c, err := v.CountInFolder(ctx, q.OrgID, folder, q.SignedInUser)
c, err := v.CountInFolders(ctx, q.OrgID, folders, q.SignedInUser)
if err != nil {
logger.Error("failed to count folder descendants", "error", err)
return nil, err
}
countsMap[v.Kind()] += c
}
countsMap[v.Kind()] = c
}
return countsMap, nil
}
func (s *Service) getNestedFolders(ctx context.Context, orgID int64, uid string) ([]string, error) {
result := []string{}
folders, err := s.store.GetChildren(ctx, folder.GetChildrenQuery{UID: uid, OrgID: orgID})
if err != nil {
return nil, err
}
for _, f := range folders {
result = append(result, f.UID)
subfolders, err := s.getNestedFolders(ctx, f.OrgID, f.UID)
if err != nil {
return nil, err
}
result = append(result, subfolders...)
}
return result, nil
}
// buildSaveDashboardCommand is a simplified version on DashboardServiceImpl.buildSaveDashboardCommand
// keeping only the meaningful functionality for folders
func (s *Service) buildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.SaveDashboardCommand, error) {

View File

@ -259,7 +259,6 @@ func TestIntegrationFolderService(t *testing.T) {
t.Run("When deleting folder by uid should not return access denied error", func(t *testing.T) {
f := folder.NewFolder(util.GenerateShortUID(), "")
f.UID = util.GenerateShortUID()
folderStore.On("GetFolders", mock.Anything, orgID, []string{f.UID}).Return(map[string]*folder.Folder{f.UID: f}, nil)
folderStore.On("GetFolderByUID", mock.Anything, orgID, f.UID).Return(f, nil)
var actualCmd *dashboards.DeleteDashboardCommand
@ -458,7 +457,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
t.Cleanup(func() {
guardian.New = origNewGuardian
for _, ancestor := range ancestors {
err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
err := serviceWithFlagOn.store.Delete(context.Background(), []string{ancestor.UID}, orgID)
assert.NoError(t, err)
}
})
@ -538,7 +537,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
t.Cleanup(func() {
guardian.New = origNewGuardian
for _, ancestor := range ancestors {
err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
err := serviceWithFlagOn.store.Delete(context.Background(), []string{ancestor.UID}, orgID)
assert.NoError(t, err)
}
})
@ -1392,17 +1391,12 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
t.Cleanup(func() {
guardian.New = origNewGuardian
for _, ancestor := range ancestorFoldersWithPermissions {
err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
assert.NoError(t, err)
toDelete := make([]string, 0, len(ancestorFoldersWithPermissions)+len(ancestorFoldersWithoutPermissions))
for _, ancestor := range append(ancestorFoldersWithPermissions, ancestorFoldersWithoutPermissions...) {
toDelete = append(toDelete, ancestor.UID)
}
})
t.Cleanup(func() {
guardian.New = origNewGuardian
for _, ancestor := range ancestorFoldersWithoutPermissions {
err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID)
err := serviceWithFlagOn.store.Delete(context.Background(), toDelete, orgID)
assert.NoError(t, err)
}
})
})
@ -1435,14 +1429,12 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
t.Cleanup(func() {
guardian.New = origNewGuardian
for _, f := range tree1 {
err := serviceWithFlagOn.store.Delete(context.Background(), f.UID, orgID)
assert.NoError(t, err)
toDelete := make([]string, 0, len(tree1)+len(tree2))
for _, f := range append(tree1, tree2...) {
toDelete = append(toDelete, f.UID)
}
for _, f := range tree2 {
err := serviceWithFlagOn.store.Delete(context.Background(), f.UID, orgID)
err := serviceWithFlagOn.store.Delete(context.Background(), toDelete, orgID)
assert.NoError(t, err)
}
})
testCases := []struct {

View File

@ -83,11 +83,20 @@ func (ss *sqlStore) Create(ctx context.Context, cmd folder.CreateFolderCommand)
return foldr.WithURL(), err
}
func (ss *sqlStore) Delete(ctx context.Context, uid string, orgID int64) error {
func (ss *sqlStore) Delete(ctx context.Context, UIDs []string, orgID int64) error {
if len(UIDs) == 0 {
return nil
}
return ss.db.WithDbSession(ctx, func(sess *db.Session) error {
_, err := sess.Exec("DELETE FROM folder WHERE uid=? AND org_id=?", uid, orgID)
s := fmt.Sprintf("DELETE FROM folder WHERE org_id=? AND uid IN (%s)", strings.Repeat("?, ", len(UIDs)-1)+"?")
sqlArgs := make([]any, 0, len(UIDs)+2)
sqlArgs = append(sqlArgs, s, orgID)
for _, uid := range UIDs {
sqlArgs = append(sqlArgs, uid)
}
_, err := sess.Exec(sqlArgs...)
if err != nil {
return folder.ErrDatabaseError.Errorf("failed to delete folder: %w", err)
return folder.ErrDatabaseError.Errorf("failed to delete folders: %w", err)
}
return nil
})
@ -331,6 +340,7 @@ func (ss *sqlStore) getParentsMySQL(ctx context.Context, q folder.GetParentsQuer
return util.Reverse(folders), err
}
// TODO use a single query to get the height of a folder
func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) {
height := -1
queue := []string{foldrUID}
@ -460,6 +470,65 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde
return folders, nil
}
func (ss *sqlStore) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) {
var folders []*folder.Folder
recursiveQueriesAreSupported, err := ss.db.RecursiveQueriesAreSupported()
if err != nil {
return nil, err
}
switch recursiveQueriesAreSupported {
case true:
recQuery := `
WITH RECURSIVE RecQry AS (
SELECT * FROM folder WHERE parent_uid = ? AND org_id = ?
UNION ALL SELECT f.* FROM folder f INNER JOIN RecQry r ON f.parent_uid = r.uid and f.org_id = r.org_id
)
SELECT * FROM RecQry;
`
if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
err := sess.SQL(recQuery, ancestor_uid, orgID).Find(&folders)
if err != nil {
return folder.ErrDatabaseError.Errorf("failed to get folder descendants: %w", err)
}
return nil
}); err != nil {
return nil, err
}
default:
// this is suboptimal because results is full table scan on f0
// but it's the best we can do without recursive CTE
if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
s := strings.Builder{}
args := make([]any, 0, 1+folder.MaxNestedFolderDepth)
args = append(args, orgID)
s.WriteString(`SELECT f0.id, f0.org_id, f0.uid, f0.parent_uid, f0.title, f0.description, f0.created, f0.updated`)
s.WriteString(` FROM folder f0`)
s.WriteString(getFullpathJoinsSQL())
s.WriteString(` WHERE f0.org_id=?`)
s.WriteString(` AND (`)
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
if i > 1 {
s.WriteString(` OR `)
}
s.WriteString(fmt.Sprintf(`f%d.uid=?`, i))
args = append(args, ancestor_uid)
}
s.WriteString(`)`)
return sess.SQL(s.String(), args...).Find(&folders)
}); err != nil {
return nil, err
}
}
// Add URLs
for i, f := range folders {
folders[i] = f.WithURL()
}
return folders, nil
}
func getFullpathSQL(dialect migrator.Dialect) string {
concatCols := make([]string, 0, folder.MaxNestedFolderDepth)
concatCols = append(concatCols, "COALESCE(REPLACE(f0.title, '/', '\\/'), '')")

View File

@ -64,7 +64,7 @@ func TestIntegrationCreate(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() {
err := folderStore.Delete(context.Background(), f.UID, orgID)
err := folderStore.Delete(context.Background(), []string{f.UID}, orgID)
require.NoError(t, err)
})
@ -102,7 +102,7 @@ func TestIntegrationCreate(t *testing.T) {
assert.NotEmpty(t, parent.URL)
t.Cleanup(func() {
err := folderStore.Delete(context.Background(), parent.UID, orgID)
err := folderStore.Delete(context.Background(), []string{parent.UID}, orgID)
require.NoError(t, err)
})
assertAncestorUIDs(t, folderStore, parent, []string{folder.GeneralFolderUID})
@ -117,7 +117,7 @@ func TestIntegrationCreate(t *testing.T) {
})
require.NoError(t, err)
t.Cleanup(func() {
err := folderStore.Delete(context.Background(), f.UID, orgID)
err := folderStore.Delete(context.Background(), []string{f.UID}, orgID)
require.NoError(t, err)
})
@ -165,7 +165,7 @@ func TestIntegrationDelete(t *testing.T) {
t.Cleanup(func() {
for _, uid := range ancestorUIDs[1:] {
err := folderStore.Delete(context.Background(), uid, orgID)
err := folderStore.Delete(context.Background(), []string{uid}, orgID)
require.NoError(t, err)
}
})
@ -178,7 +178,7 @@ func TestIntegrationDelete(t *testing.T) {
*/
t.Run("deleting a leaf folder should succeed", func(t *testing.T) {
err := folderStore.Delete(context.Background(), ancestorUIDs[len(ancestorUIDs)-1], orgID)
err := folderStore.Delete(context.Background(), []string{ancestorUIDs[len(ancestorUIDs)-1]}, orgID)
require.NoError(t, err)
children, err := folderStore.GetChildren(context.Background(), folder.GetChildrenQuery{
@ -221,7 +221,7 @@ func TestIntegrationUpdate(t *testing.T) {
require.NoError(t, err)
require.Equal(t, f.ParentUID, parent.UID)
t.Cleanup(func() {
err := folderStore.Delete(context.Background(), f.UID, orgID)
err := folderStore.Delete(context.Background(), []string{f.UID}, orgID)
require.NoError(t, err)
})
@ -393,7 +393,7 @@ func TestIntegrationGet(t *testing.T) {
})
t.Cleanup(func() {
err := folderStore.Delete(context.Background(), f.UID, orgID)
err := folderStore.Delete(context.Background(), []string{f.UID}, orgID)
require.NoError(t, err)
})
@ -489,7 +489,7 @@ func TestIntegrationGetParents(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() {
err := folderStore.Delete(context.Background(), f.UID, orgID)
err := folderStore.Delete(context.Background(), []string{f.UID}, orgID)
require.NoError(t, err)
})
@ -561,7 +561,7 @@ func TestIntegrationGetChildren(t *testing.T) {
t.Cleanup(func() {
for _, uid := range treeLeaves {
err := folderStore.Delete(context.Background(), uid, orgID)
err := folderStore.Delete(context.Background(), []string{uid}, orgID)
require.NoError(t, err)
}
})
@ -778,7 +778,7 @@ func TestIntegrationGetFolders(t *testing.T) {
t.Cleanup(func() {
for _, uid := range uids {
err := folderStore.Delete(context.Background(), uid, orgID)
err := folderStore.Delete(context.Background(), []string{uid}, orgID)
require.NoError(t, err)
}
})

View File

@ -23,8 +23,8 @@ type store interface {
// Create creates a folder and returns the newly-created folder.
Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error)
// Delete deletes a folder from the folder store.
Delete(ctx context.Context, uid string, orgID int64) error
// Delete folders with the specified UIDs and orgID from the folder store.
Delete(ctx context.Context, UIDs []string, orgID int64) error
// Update updates the given folder's UID, Title, and Description (update mode).
// If the NewParentUID field is not nil, it updates also the parent UID (move mode).
@ -48,4 +48,6 @@ type store interface {
// GetFolders returns folders with given uids
GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error)
// GetDescendants returns all descendants of a folder
GetDescendants(ctx context.Context, orgID int64, anchestor_uid string) ([]*folder.Folder, error)
}

View File

@ -28,7 +28,7 @@ func (f *fakeStore) Create(ctx context.Context, cmd folder.CreateFolderCommand)
return f.ExpectedFolder, f.ExpectedError
}
func (f *fakeStore) Delete(ctx context.Context, uid string, orgID int64) error {
func (f *fakeStore) Delete(ctx context.Context, UIDs []string, orgID int64) error {
f.DeleteCalled = true
return f.ExpectedError
}
@ -60,3 +60,7 @@ func (f *fakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64
func (f *fakeStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) {
return f.ExpectedFolders, f.ExpectedError
}
func (f *fakeStore) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) {
return f.ExpectedFolders, f.ExpectedError
}

View File

@ -7,7 +7,7 @@ import (
)
type RegistryService interface {
DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) error
CountInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) (int64, error)
DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) error
CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error)
Kind() string
}

View File

@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
@ -188,25 +190,41 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S
// 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 identity.Requester) (int64, error) {
func (lps LibraryPanelService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) {
if len(folderUIDs) == 0 {
return 0, nil
}
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})
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryPanels).Inc()
// the sequential IDs for the respective entries of dashboard and folder tables are different,
// so we need to get the folder ID from the dashboard table
// TODO: In the future, we should consider adding a folder UID column to the library_element table
// and use that instead of the folder ID.
s := fmt.Sprintf(`SELECT COUNT(*) FROM library_element
WHERE org_id = ? AND folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (%s)) AND kind = ?`, strings.Repeat("?,", len(folderUIDs)-1)+"?")
args := make([]interface{}, 0, len(folderUIDs)+2)
args = append(args, orgID, orgID)
for _, folderUID := range folderUIDs {
args = append(args, folderUID)
}
args = append(args, int64(model.PanelElement))
_, err := sess.SQL(s, args...).Get(&count)
if err != nil {
return err
}
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryPanels).Inc()
// nolint:staticcheck
q := sess.Table("library_element").Where("org_id = ?", u.GetOrgID()).
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 identity.Requester) error {
return lps.LibraryElementService.DeleteLibraryElementsInFolder(ctx, user, folderUID)
func (lps LibraryPanelService) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) error {
for _, folderUID := range folderUIDs {
if err := lps.LibraryElementService.DeleteLibraryElementsInFolder(ctx, user, folderUID); err != nil {
return err
}
}
return nil
}
// Kind returns the name of the library panel type of entity.

View File

@ -326,14 +326,14 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
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)
count, err := sc.lps.CountInFolders(context.Background(), sc.user.OrgID, []string{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)
err := sc.lps.DeleteInFolders(context.Background(), sc.user.OrgID, []string{sc.folder.UID}, sc.user)
require.NoError(t, err)
_, err = sc.elementService.GetElement(sc.ctx, sc.user,

View File

@ -412,10 +412,10 @@ func TestDashAlertPermissionMigration(t *testing.T) {
},
},
dashboards: []*dashboards.Dashboard{
genDashboard(t, 3, "d_1", "", 1),
genDashboard(t, 4, "d_2", "", 1),
genDashboard(t, 5, "d_3", "", 2),
genDashboard(t, 6, "d_4", "", 2),
genDashboard(t, 3, "d_1", "1", 1),
genDashboard(t, 4, "d_2", "1", 1),
genDashboard(t, 5, "d_3", "2", 2),
genDashboard(t, 6, "d_4", "2", 2),
},
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
"d_1": {

View File

@ -1398,6 +1398,7 @@ func TestCommonServicePatterns(t *testing.T) {
d1Copy := *d1
//nolint:staticcheck
d1Copy.FolderID = f2.ID
d1Copy.FolderUID = f2.UID
_, err := x.ID(d1.ID).Update(d1Copy)
return err
},

View File

@ -33,7 +33,7 @@ func TestSilences(t *testing.T) {
o := createOrg(t, 1)
folder1 := createFolder(t, 1, o.ID, "folder-1")
dash1 := createDashboard(t, 3, o.ID, "dash1", "", folder1.ID, nil)
dash1 := createDashboard(t, 3, o.ID, "dash1", folder1.UID, folder1.ID, nil)
silenceTests := []struct {
name string

View File

@ -318,11 +318,18 @@ func newTitlesOverlapExisting(rules []ngmodels.UpdateRule) bool {
// CountInFolder is a handler for retrieving the number of alert rules of
// specific organisation associated with a given namespace (parent folder).
func (st DBstore) CountInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) (int64, error) {
func (st DBstore) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) {
if len(folderUIDs) == 0 {
return 0, nil
}
var count int64
var err error
err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
q := sess.Table("alert_rule").Where("org_id = ?", orgID).Where("namespace_uid = ?", folderUID)
args := make([]any, 0, len(folderUIDs))
for _, folderUID := range folderUIDs {
args = append(args, folderUID)
}
q := sess.Table("alert_rule").Where("org_id = ?", orgID).Where(fmt.Sprintf("namespace_uid IN (%s)", strings.Repeat("?,", len(folderUIDs)-1)+"?"), args...)
count, err = q.Count()
return err
})
@ -577,7 +584,8 @@ 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, user identity.Requester) error {
func (st DBstore) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) error {
for _, folderUID := range folderUIDs {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID))
canSave, err := st.AccessControl.Evaluate(ctx, user, evaluator)
if err != nil {
@ -607,6 +615,7 @@ func (st DBstore) DeleteInFolder(ctx context.Context, orgID int64, folderUID str
if err := st.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil {
return err
}
}
return nil
}

View File

@ -458,8 +458,8 @@ func TestIntegration_CountAlertRules(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
count, err := store.CountInFolder(context.Background(),
test.query.OrgID, test.query.NamespaceUID, nil)
count, err := store.CountInFolders(context.Background(),
test.query.OrgID, []string{test.query.NamespaceUID}, nil)
if test.expectErr {
require.Error(t, err)
} else {
@ -486,7 +486,7 @@ func TestIntegration_DeleteInFolder(t *testing.T) {
t.Run("should not be able to delete folder without permissions to delete rules", func(t *testing.T) {
store.AccessControl = acmock.New()
err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{})
err := store.DeleteInFolders(context.Background(), rule.OrgID, []string{rule.NamespaceUID}, &user.SignedInUser{})
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
})
@ -494,10 +494,10 @@ func TestIntegration_DeleteInFolder(t *testing.T) {
store.AccessControl = acmock.New().WithPermissions([]accesscontrol.Permission{
{Action: accesscontrol.ActionAlertingRuleDelete, Scope: dashboards.ScopeFoldersAll},
})
err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{})
err := store.DeleteInFolders(context.Background(), rule.OrgID, []string{rule.NamespaceUID}, &user.SignedInUser{})
require.NoError(t, err)
c, err := store.CountInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{})
c, err := store.CountInFolders(context.Background(), rule.OrgID, []string{rule.NamespaceUID}, &user.SignedInUser{})
require.NoError(t, err)
require.Equal(t, int64(0), c)
})

View File

@ -336,6 +336,6 @@ func (f *RuleStore) IncreaseVersionForAllRulesInNamespace(_ context.Context, org
return result, nil
}
func (f *RuleStore) CountInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) (int64, error) {
func (f *RuleStore) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) {
return 0, nil
}