mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
0139ac205d
commit
89d3b55bec
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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,42 +1014,58 @@ 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 {
|
||||
dashboard := dashboards.Dashboard{OrgID: req.OrgID}
|
||||
has, err := sess.Where("uid = ? AND org_id = ?", req.FolderUID, req.OrgID).Get(&dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return dashboards.ErrFolderNotFound
|
||||
}
|
||||
// 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("org_id = ? AND uid = ?", req.OrgID, folderUID).Get(&dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return dashboards.ErrFolderNotFound
|
||||
}
|
||||
|
||||
if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sess.Where("folder_id = ? AND org_id = ? AND is_folder = ?", dashboard.ID, dashboard.OrgID, false).Delete(&dashboards.Dashboard{})
|
||||
return err
|
||||
_, 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
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
OrgID: 1,
|
||||
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))
|
||||
})
|
||||
|
@ -220,6 +220,7 @@ type DashboardProvisioning struct {
|
||||
|
||||
type DeleteDashboardCommand struct {
|
||||
ID int64
|
||||
UID string
|
||||
OrgID int64
|
||||
ForceDeleteFolderRules bool
|
||||
}
|
||||
@ -322,9 +323,8 @@ 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
|
||||
OrgID int64
|
||||
FolderUIDs []string
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
func FromDashboard(dash *Dashboard) *folder.Folder {
|
||||
@ -345,8 +345,8 @@ func FromDashboard(dash *Dashboard) *folder.Folder {
|
||||
}
|
||||
|
||||
type DeleteDashboardsInFolderRequest struct {
|
||||
FolderUID string
|
||||
OrgID int64
|
||||
FolderUIDs []string
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -748,72 +748,62 @@ 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...)
|
||||
folders = append(folders, descendants...)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if cmd.ForceDeleteRules {
|
||||
if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
alertRuleSrv, ok := s.registry[entity.StandardKindAlertRule]
|
||||
if !ok {
|
||||
return folder.ErrInternal.Errorf("no alert rule service found in registry")
|
||||
}
|
||||
alertRulesInFolder, err := alertRuleSrv.CountInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("failed to count alert rules in folder", "error", err)
|
||||
return err
|
||||
}
|
||||
if alertRulesInFolder > 0 {
|
||||
return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder)
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.legacyDelete(ctx, cmd, dashFolder); err != nil {
|
||||
if cmd.ForceDeleteRules {
|
||||
if err := s.deleteChildrenInFolder(ctx, cmd.OrgID, folders, cmd.SignedInUser); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
alertRuleSrv, ok := s.registry[entity.StandardKindAlertRule]
|
||||
if !ok {
|
||||
return folder.ErrInternal.Errorf("no alert rule service found in registry")
|
||||
}
|
||||
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
|
||||
}
|
||||
if alertRulesInFolder > 0 {
|
||||
return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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,54 +935,36 @@ 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)
|
||||
if err != nil {
|
||||
logger.Error("failed to count folder descendants", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
result = append(result, subfolders...)
|
||||
countsMap[v.Kind()] = c
|
||||
}
|
||||
return result, nil
|
||||
return countsMap, nil
|
||||
}
|
||||
|
||||
// buildSaveDashboardCommand is a simplified version on DashboardServiceImpl.buildSaveDashboardCommand
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origNewGuardian
|
||||
for _, ancestor := range ancestorFoldersWithoutPermissions {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
for _, f := range tree2 {
|
||||
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)
|
||||
}
|
||||
err := serviceWithFlagOn.store.Delete(context.Background(), toDelete, orgID)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
|
@ -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, '/', '\\/'), '')")
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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,35 +584,37 @@ 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 {
|
||||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID))
|
||||
canSave, err := st.AccessControl.Evaluate(ctx, user, evaluator)
|
||||
if err != nil {
|
||||
st.Logger.Error("Failed to evaluate access control", "error", err)
|
||||
return err
|
||||
}
|
||||
if !canSave {
|
||||
st.Logger.Error("user is not allowed to delete alert rules in folder", "folder", folderUID, "user")
|
||||
return dashboards.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{
|
||||
OrgID: orgID,
|
||||
NamespaceUIDs: []string{folderUID},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uids := make([]string, 0, len(rules))
|
||||
for _, tgt := range rules {
|
||||
if tgt != nil {
|
||||
uids = append(uids, tgt.UID)
|
||||
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 {
|
||||
st.Logger.Error("Failed to evaluate access control", "error", err)
|
||||
return err
|
||||
}
|
||||
if !canSave {
|
||||
st.Logger.Error("user is not allowed to delete alert rules in folder", "folder", folderUID, "user")
|
||||
return dashboards.ErrFolderAccessDenied
|
||||
}
|
||||
}
|
||||
|
||||
if err := st.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil {
|
||||
return err
|
||||
rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{
|
||||
OrgID: orgID,
|
||||
NamespaceUIDs: []string{folderUID},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uids := make([]string, 0, len(rules))
|
||||
for _, tgt := range rules {
|
||||
if tgt != nil {
|
||||
uids = append(uids, tgt.UID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := st.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user