diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index fd567340110..49062997533 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -29,7 +29,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `featureHighlights` | Highlight Grafana Enterprise features | |
| `correlations` | Correlations page | Yes |
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
-| `nestedFolders` | Enable folder nesting | Yes |
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes |
| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes |
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 32cfbabd040..7198f33660e 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -80,11 +80,6 @@ export interface FeatureToggles {
*/
mysqlAnsiQuotes?: boolean;
/**
- * Enable folder nesting
- * @default true
- */
- nestedFolders?: boolean;
- /**
* Rule backtesting API for alerting
*/
alertingBacktesting?: boolean;
diff --git a/pkg/api/folder.go b/pkg/api/folder.go
index 82ccca6c8f1..958113e9f36 100644
--- a/pkg/api/folder.go
+++ b/pkg/api/folder.go
@@ -14,10 +14,8 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
- "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
- "github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -71,41 +69,32 @@ func (hs *HTTPServer) GetFolders(c *contextmodel.ReqContext) response.Response {
permission = dashboardaccess.PERMISSION_EDIT
}
- if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
- q := &folder.GetChildrenQuery{
- OrgID: c.GetOrgID(),
- Limit: c.QueryInt64("limit"),
- Page: c.QueryInt64("page"),
- UID: c.Query("parentUid"),
- Permission: permission,
- SignedInUser: c.SignedInUser,
- }
-
- folders, err := hs.folderService.GetChildren(c.Req.Context(), q)
- if err != nil {
- return apierrors.ToFolderErrorResponse(err)
- }
-
- hits := make([]dtos.FolderSearchHit, 0)
- for _, f := range folders {
- hits = append(hits, dtos.FolderSearchHit{
- ID: f.ID, // nolint:staticcheck
- UID: f.UID,
- Title: f.Title,
- ParentUID: f.ParentUID,
- ManagedBy: f.ManagedBy,
- })
- metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetFolders).Inc()
- }
-
- return response.JSON(http.StatusOK, hits)
+ q := &folder.GetChildrenQuery{
+ OrgID: c.GetOrgID(),
+ Limit: c.QueryInt64("limit"),
+ Page: c.QueryInt64("page"),
+ UID: c.Query("parentUid"),
+ Permission: permission,
+ SignedInUser: c.SignedInUser,
}
- hits, err := hs.searchFolders(c, permission)
+ folders, err := hs.folderService.GetChildren(c.Req.Context(), q)
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
+ hits := make([]dtos.FolderSearchHit, 0)
+ for _, f := range folders {
+ hits = append(hits, dtos.FolderSearchHit{
+ ID: f.ID, // nolint:staticcheck
+ UID: f.UID,
+ Title: f.Title,
+ ParentUID: f.ParentUID,
+ ManagedBy: f.ManagedBy,
+ })
+ metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetFolders).Inc()
+ }
+
return response.JSON(http.StatusOK, hits)
}
@@ -214,30 +203,25 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response {
- if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
- cmd := folder.MoveFolderCommand{}
- if err := web.Bind(c.Req, &cmd); err != nil {
- return response.Error(http.StatusBadRequest, "bad request data", err)
- }
- var err error
-
- cmd.OrgID = c.GetOrgID()
- cmd.UID = web.Params(c.Req)[":uid"]
- cmd.SignedInUser = c.SignedInUser
- theFolder, err := hs.folderService.Move(c.Req.Context(), &cmd)
- if err != nil {
- return response.ErrOrFallback(http.StatusInternalServerError, "move folder failed", err)
- }
-
- folderDTO, err := hs.newToFolderDto(c, theFolder)
- if err != nil {
- return response.Err(err)
- }
- return response.JSON(http.StatusOK, folderDTO)
+ cmd := folder.MoveFolderCommand{}
+ if err := web.Bind(c.Req, &cmd); err != nil {
+ return response.Error(http.StatusBadRequest, "bad request data", err)
}
- result := map[string]string{}
- result["message"] = "To use this service, you need to activate nested folder feature."
- return response.JSON(http.StatusNotFound, result)
+ var err error
+
+ cmd.OrgID = c.GetOrgID()
+ cmd.UID = web.Params(c.Req)[":uid"]
+ cmd.SignedInUser = c.SignedInUser
+ theFolder, err := hs.folderService.Move(c.Req.Context(), &cmd)
+ if err != nil {
+ return response.ErrOrFallback(http.StatusInternalServerError, "move folder failed", err)
+ }
+
+ folderDTO, err := hs.newToFolderDto(c, theFolder)
+ if err != nil {
+ return response.Err(err)
+ }
+ return response.JSON(http.StatusOK, folderDTO)
}
// swagger:route PUT /folders/{folder_uid} folders updateFolder
@@ -396,10 +380,6 @@ func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, f *folder.Folde
return dtos.Folder{}, err
}
- if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
- return folderDTO, nil
- }
-
parents, err := hs.folderService.GetParents(ctx, folder.GetParentsQuery{UID: f.UID, OrgID: f.OrgID})
if err != nil {
// log the error instead of failing
@@ -445,36 +425,6 @@ func (hs *HTTPServer) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.
return metadata, nil
}
-func (hs *HTTPServer) searchFolders(c *contextmodel.ReqContext, permission dashboardaccess.PermissionType) ([]dtos.FolderSearchHit, error) {
- searchQuery := search.Query{
- SignedInUser: c.SignedInUser,
- DashboardIds: make([]int64, 0),
- FolderIds: make([]int64, 0), // nolint:staticcheck
- Limit: c.QueryInt64("limit"),
- OrgId: c.GetOrgID(),
- Type: "dash-folder",
- Permission: permission,
- Page: c.QueryInt64("page"),
- }
-
- hits, err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery)
- if err != nil {
- return nil, err
- }
-
- folderHits := make([]dtos.FolderSearchHit, 0)
- for _, hit := range hits {
- folderHits = append(folderHits, dtos.FolderSearchHit{
- ID: hit.ID, // nolint:staticcheck
- UID: hit.UID,
- Title: hit.Title,
- })
- metrics.MFolderIDsAPICount.WithLabelValues(metrics.SearchFolders).Inc()
- }
-
- return folderHits, nil
-}
-
// swagger:parameters getFolders
type GetFoldersParams struct {
// Limit the maximum number of folders to return
diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go
index 75aab0776c8..95e9831f2b2 100644
--- a/pkg/api/folder_bench_test.go
+++ b/pkg/api/folder_bench_test.go
@@ -107,80 +107,50 @@ func BenchmarkFolderListAndSearch(b *testing.B) {
features featuremgmt.FeatureToggles
}{
{
- desc: "impl=default nested_folders=on get root folders",
+ desc: "impl=default get root folders",
url: "/api/folders",
expectedLen: LEVEL0_FOLDER_NUM + 1, // for shared with me folder
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
+ features: featuremgmt.WithFeatures(),
},
{
- desc: "impl=default nested_folders=on get subfolders",
+ desc: "impl=default get subfolders",
url: "/api/folders?parentUid=folder0",
expectedLen: LEVEL1_FOLDER_NUM,
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
- },
- {
- desc: "impl=default nested_folders=on list all inherited dashboards",
- url: "/api/search?type=dash-db&limit=5000",
- expectedLen: withLimit(all),
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
- },
- {
- desc: "impl=permissionsFilterRemoveSubquery nested_folders=on list all inherited dashboards",
- url: "/api/search?type=dash-db&limit=5000",
- expectedLen: withLimit(all),
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
- },
- {
- desc: "impl=default nested_folders=on search for pattern",
- url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
- expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
- },
- {
- desc: "impl=permissionsFilterRemoveSubquery nested_folders=on search for pattern",
- url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
- expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
- },
- {
- desc: "impl=default nested_folders=on search for specific dashboard",
- url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
- expectedLen: 1,
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
- },
- {
- desc: "impl=permissionsFilterRemoveSubquery nested_folders=on search for specific dashboard",
- url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
- expectedLen: 1,
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
- },
- {
- desc: "impl=default nested_folders=off get root folders",
- url: "/api/folders?limit=5000",
- expectedLen: withLimit(LEVEL0_FOLDER_NUM),
features: featuremgmt.WithFeatures(),
},
{
- desc: "impl=default nested_folders=off list all dashboards",
+ desc: "impl=defaultlist all inherited dashboards",
url: "/api/search?type=dash-db&limit=5000",
- expectedLen: withLimit(LEVEL0_FOLDER_NUM * LEVEL0_DASHBOARD_NUM),
+ expectedLen: withLimit(all),
features: featuremgmt.WithFeatures(),
},
{
- desc: "impl=permissionsFilterRemoveSubquery nested_folders=off list all dashboards",
+ desc: "impl=permissionsFilterRemoveSubquery list all inherited dashboards",
url: "/api/search?type=dash-db&limit=5000",
- expectedLen: withLimit(LEVEL0_FOLDER_NUM * LEVEL0_DASHBOARD_NUM),
+ expectedLen: withLimit(all),
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
- desc: "impl=default nested_folders=off search specific dashboard",
- url: "/api/search?type=dash-db&query=dashboard_0_0",
+ desc: "impl=default search for pattern",
+ url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
+ expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
+ features: featuremgmt.WithFeatures(),
+ },
+ {
+ desc: "impl=permissionsFilterRemoveSubquery search for pattern",
+ url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
+ expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
+ features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
+ },
+ {
+ desc: "impl=default search for specific dashboard",
+ url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
expectedLen: 1,
features: featuremgmt.WithFeatures(),
},
{
- desc: "impl=permissionsFilterRemoveSubquery nested_folders=off search specific dashboard",
- url: "/api/search?type=dash-db&query=dashboard_0_0",
+ desc: "impl=permissionsFilterRemoveSubquery search for specific dashboard",
+ url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
expectedLen: 1,
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go
index c02cad02628..12d65a06d29 100644
--- a/pkg/api/folder_test.go
+++ b/pkg/api/folder_test.go
@@ -43,7 +43,6 @@ func TestFoldersCreateAPIEndpoint(t *testing.T) {
expectedFolder *folder.Folder
expectedFolderSvcError error
permissions []accesscontrol.Permission
- withNestedFolders bool
input string
}
tcs := []testCase{
@@ -119,10 +118,6 @@ func TestFoldersCreateAPIEndpoint(t *testing.T) {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
-
- if tc.withNestedFolders {
- hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
- }
hs.folderService = folderService
hs.folderPermissionsService = folderPermService
hs.accesscontrolService = actest.FakeService{}
@@ -255,7 +250,7 @@ func testDescription(description string, expectedErr error) string {
func TestHTTPServer_FolderMetadata(t *testing.T) {
folderService := &foldertest.FakeService{}
- features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
+ features := featuremgmt.WithFeatures()
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.folderService = folderService
@@ -292,7 +287,6 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
t.Run("Should attach access control metadata to folder response with permissions cascading from nested folders", func(t *testing.T) {
folderService.ExpectedFolder = &folder.Folder{UID: "folderUid"}
folderService.ExpectedFolders = []*folder.Folder{{UID: "parentUid"}}
- features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
defer func() {
features = featuremgmt.WithFeatures()
folderService.ExpectedFolders = nil
@@ -386,7 +380,6 @@ func TestFolderMoveAPIEndpoint(t *testing.T) {
for _, tc := range tcs {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
- hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
hs.folderService = folderService
})
@@ -423,7 +416,6 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
type testCase struct {
description string
URL string
- features featuremgmt.FeatureToggles
expectedCode int
expectedParentUIDs []string
expectedParentOrgIDs []int64
@@ -435,7 +427,6 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
description: "get folder by UID should return parent folders if nested folder are enabled",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{"parent", "subfolder"},
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{"parent title", "subfolder title"},
@@ -449,7 +440,6 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
description: "get folder by UID should return parent folders redacted if nested folder are enabled and user does not have read access to parent folders",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{REDACTED, REDACTED},
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{REDACTED, REDACTED},
@@ -461,7 +451,6 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
description: "get folder by UID should return some parent folder titles and some parent folders as redacted if nested folder are enabled and user only has read access to some parent folders",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{REDACTED, "subfolder"},
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{REDACTED, "subfolder title"},
@@ -470,24 +459,11 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("subfolder")},
},
},
- {
- description: "get folder by UID should not return parent folders if nested folder are disabled",
- URL: "/api/folders/uid",
- expectedCode: http.StatusOK,
- features: featuremgmt.WithFeatures(),
- expectedParentUIDs: []string{},
- expectedParentOrgIDs: []int64{0, 0},
- expectedParentTitles: []string{},
- permissions: []accesscontrol.Permission{
- {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceAllScope()},
- },
- },
}
for _, tc := range tcs {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
- hs.Features = tc.features
hs.folderService = folderService
})
@@ -619,8 +595,6 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) {
},
}
- featuresArr := []any{featuremgmt.FlagNestedFolders}
-
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.folderService = &foldertest.FakeService{
@@ -634,9 +608,7 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) {
hs.userService = &usertest.FakeUserService{
ExpectedUser: testuser,
}
- hs.Features = featuremgmt.WithFeatures(
- featuresArr...,
- )
+ hs.Features = featuremgmt.WithFeatures()
hs.clientConfigProvider = mockClientConfigProvider{
host: folderApiServerMock.URL,
}
@@ -697,7 +669,6 @@ func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
- hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
hs.folderService = folderService
hs.folderPermissionsService = folderPermService
hs.accesscontrolService = actest.FakeService{}
diff --git a/pkg/registry/apis/folders/authorizer_test.go b/pkg/registry/apis/folders/authorizer_test.go
index c594fba54ec..290f6e44252 100644
--- a/pkg/registry/apis/folders/authorizer_test.go
+++ b/pkg/registry/apis/folders/authorizer_test.go
@@ -162,7 +162,7 @@ func TestLegacyAuthorizer(t *testing.T) {
},
}
- authz := newLegacyAuthorizer(acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders")))
+ authz := newLegacyAuthorizer(acimpl.ProvideAccessControl(featuremgmt.WithFeatures()))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/registry/apis/folders/folder_storage.go b/pkg/registry/apis/folders/folder_storage.go
index 2f3ab79873e..b2084051824 100644
--- a/pkg/registry/apis/folders/folder_storage.go
+++ b/pkg/registry/apis/folders/folder_storage.go
@@ -153,7 +153,7 @@ func (s *folderStorage) setDefaultFolderPermissions(ctx context.Context, orgID i
})
}
isNested := parentUID != ""
- if !isNested || !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
+ if !isNested {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go
index 9f489a79dac..9f54215a061 100644
--- a/pkg/services/accesscontrol/acimpl/service.go
+++ b/pkg/services/accesscontrol/acimpl/service.go
@@ -147,10 +147,7 @@ func (s *Service) getUserPermissions(ctx context.Context, user identity.Requeste
permissions = append(permissions, basicRole.Permissions...)
}
}
-
- if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
- permissions = append(permissions, SharedWithMeFolderPermission)
- }
+ permissions = append(permissions, SharedWithMeFolderPermission)
// we don't care about the error here, if this fails we get 0 and no
// permission assigned to user will be returned, only for org role.
@@ -232,9 +229,7 @@ func (s *Service) getUserDirectPermissions(ctx context.Context, user identity.Re
}
permissions = s.actionResolver.ExpandActionSets(permissions)
- if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
- permissions = append(permissions, SharedWithMeFolderPermission)
- }
+ permissions = append(permissions, SharedWithMeFolderPermission)
return permissions, nil
}
diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go
index 8a086eb5562..5c6bb1e7a00 100644
--- a/pkg/services/accesscontrol/acimpl/service_test.go
+++ b/pkg/services/accesscontrol/acimpl/service_test.go
@@ -933,7 +933,8 @@ func TestIntegrationService_SaveExternalServiceRole(t *testing.T) {
// Check that the permissions and assignment are stored correctly
perms, errGetPerms := ac.getUserPermissions(ctx, &user.SignedInUser{OrgID: r.cmd.AssignmentOrgID, UserID: 2}, accesscontrol.Options{})
require.NoError(t, errGetPerms)
- assert.ElementsMatch(t, r.cmd.Permissions, perms)
+ // shared with me is added by default for all users in pkg/services/accesscontrol/acimpl/service.go
+ assert.Equal(t, append([]accesscontrol.Permission{{Action: "folders:read", Scope: "folders:uid:sharedwithme"}}, r.cmd.Permissions...), perms)
}
})
}
@@ -989,7 +990,8 @@ func TestIntegrationService_DeleteExternalServiceRole(t *testing.T) {
// Check that the permissions and assignment are removed correctly
perms, errGetPerms := ac.getUserPermissions(ctx, &user.SignedInUser{OrgID: tt.initCmd.AssignmentOrgID, UserID: 2}, accesscontrol.Options{})
require.NoError(t, errGetPerms)
- assert.Empty(t, perms)
+ // shared with me is added by default for all users in pkg/services/accesscontrol/acimpl/service.go
+ assert.Equal(t, []accesscontrol.Permission{{Action: "folders:read", Scope: "folders:uid:sharedwithme"}}, perms)
}
})
}
diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go
index 13e874e4fad..a8c7419921d 100644
--- a/pkg/services/dashboards/database/database.go
+++ b/pkg/services/dashboards/database/database.go
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
+ "github.com/grafana/grafana/pkg/infra/slugify"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
@@ -831,10 +832,9 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
if len(query.FolderUIDs) > 0 {
filters = append(filters, searchstore.FolderUIDFilter{
- Dialect: d.store.GetDialect(),
- OrgID: orgID,
- UIDs: query.FolderUIDs,
- NestedFoldersEnabled: d.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders),
+ Dialect: d.store.GetDialect(),
+ OrgID: orgID,
+ UIDs: query.FolderUIDs,
})
}
@@ -900,6 +900,9 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
if item.Term != "" {
item.Tags = append(item.Tags, item.Term)
}
+ if item.FolderTitle != "" {
+ item.FolderSlug = slugify.Slugify(item.FolderTitle)
+ }
seen[item.ID] = len(uniqueRes)
uniqueRes = append(uniqueRes, item)
}
diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go
index dfe02a39416..bc1285ca199 100644
--- a/pkg/services/dashboards/database/database_folder_test.go
+++ b/pkg/services/dashboards/database/database_folder_test.go
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
+ "github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
@@ -30,13 +31,16 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
var flder, dashInRoot, childDash *dashboards.Dashboard
var currentUser *user.SignedInUser
var dashboardStore dashboards.Store
+ var folderStore *folderimpl.FolderStoreImpl
setup := func() {
sqlStore, cfg = db.InitTestDBWithCfg(t)
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
- flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp")
+ folderStore = folderimpl.ProvideStore(sqlStore)
+ require.NoError(t, err)
+ flder = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "prod", "webapp")
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod", "webapp")
childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, flder.ID, flder.UID, false, "prod", "webapp")
insertTestDashboard(t, dashboardStore, "test dash 45", 1, flder.ID, flder.UID, false, "prod")
@@ -132,8 +136,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
sqlStore, cfg = db.InitTestDBWithCfg(t)
var err error
require.NoError(t, err)
- folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod")
- folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, "", true, "prod")
+ folder1 = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "prod")
+ folder2 = insertTestDashFolder(t, dashboardStore, folderStore, "2 test dash folder", 1, 0, "", "prod")
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod")
childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.ID, folder1.UID, false, "prod")
childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.ID, folder2.UID, false, "prod")
@@ -190,8 +194,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
})
t.Run("and a dashboard is moved from folder with acl to the folder without an acl", func(t *testing.T) {
setup2()
- moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID, childDash2.FolderUID)
- currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}}}
+ moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID, childDash2.UID)
+ currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID), dashboards.ScopeDashboardsProvider.GetResourceScopeUID(folder2.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}}}
actest.AddUserPermissionToDB(t, sqlStore, currentUser)
t.Run("should return folder without acl and its children", func(t *testing.T) {
@@ -202,7 +206,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
}
hits, err := testSearchDashboards(dashboardStore, query)
require.NoError(t, err)
- assert.Equal(t, len(hits), 4)
+ assert.Equal(t, 4, len(hits))
assert.Equal(t, hits[0].ID, folder2.ID)
assert.Equal(t, hits[1].ID, childDash1.ID)
assert.Equal(t, hits[2].ID, childDash2.ID)
diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go
index a940e873e0b..88cd91aaced 100644
--- a/pkg/services/dashboards/database/database_test.go
+++ b/pkg/services/dashboards/database/database_test.go
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
+ "github.com/grafana/grafana/pkg/services/folder/folderimpl"
libmodel "github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search/model"
@@ -51,7 +52,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
// test dash 23
// test dash 45
// test dash 67
- savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp")
+ folderStore := folderimpl.ProvideStore(sqlStore)
+ savedFolder = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "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")
savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod")
@@ -814,7 +816,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
- features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch)
+ features := featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
@@ -931,7 +933,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
}
sqlStore, cfg := db.InitTestDBWithCfg(t)
- features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch)
+ features := featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
@@ -970,110 +972,89 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
folderIDs []int64
folderUIDs []string
query string
- expectedResult map[string][]res
+ expectedResult []res
typ string
}{
{
- desc: "find dashboard under general using folder UID",
- folderUIDs: []string{folder.GeneralFolderUID},
- typ: searchstore.TypeDashboard,
- expectedResult: map[string][]res{
- "": {{title: "dashboard under general"}},
- featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}},
- },
+ desc: "find dashboard under general using folder UID",
+ folderUIDs: []string{folder.GeneralFolderUID},
+ typ: searchstore.TypeDashboard,
+ expectedResult: []res{{title: "dashboard under general"}},
},
{
- desc: "find dashboard under general using folder UID",
- folderUIDs: []string{folder.GeneralFolderUID},
- typ: searchstore.TypeDashboard,
- expectedResult: map[string][]res{
- "": {{title: "dashboard under general"}},
- featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}},
- },
+ desc: "find dashboard under general using folder UID",
+ folderUIDs: []string{folder.GeneralFolderUID},
+ typ: searchstore.TypeDashboard,
+ expectedResult: []res{{title: "dashboard under general"}},
},
{
- desc: "find dashboard under f0 using folder UID",
- folderUIDs: []string{f0.UID},
- typ: searchstore.TypeDashboard,
- expectedResult: map[string][]res{
- "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}},
- featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}},
- },
+ desc: "find dashboard under f0 using folder UID",
+ folderUIDs: []string{f0.UID},
+ typ: searchstore.TypeDashboard,
+ expectedResult: []res{{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}},
},
{
desc: "find dashboard under f0 or f1 using folder UID",
folderUIDs: []string{f0.UID, f1.UID},
typ: searchstore.TypeDashboard,
- expectedResult: map[string][]res{
- "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
- {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}},
- featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
- {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}},
+ expectedResult: []res{
+ {title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
+ {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
},
},
{
desc: "find dashboard under general or f0 using folder UID",
folderUIDs: []string{folder.GeneralFolderUID, f0.UID},
typ: searchstore.TypeDashboard,
- expectedResult: map[string][]res{
- "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
- {title: "dashboard under general"}},
- featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
- {title: "dashboard under general"}},
+ expectedResult: []res{
+ {title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
+ {title: "dashboard under general"},
},
},
{
desc: "find dashboard under general or f0 or f1 using folder UID",
folderUIDs: []string{folder.GeneralFolderUID, f0.UID, f1.UID},
typ: searchstore.TypeDashboard,
- expectedResult: map[string][]res{
- "": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
- {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
- {title: "dashboard under general"}},
- featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
- {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
- {title: "dashboard under general"}},
+ expectedResult: []res{
+ {title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
+ {title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
+ {title: "dashboard under general"},
},
},
{
- desc: "find subfolder",
- folderUIDs: []string{f0.UID},
- typ: searchstore.TypeFolder,
- expectedResult: map[string][]res{
- "": {},
- featuremgmt.FlagNestedFolders: {{title: subfolder.Title, folderUID: f0.UID, folderTitle: f0.Title}},
- },
+ desc: "find subfolder",
+ folderUIDs: []string{f0.UID},
+ typ: searchstore.TypeFolder,
+ expectedResult: []res{{title: subfolder.Title, folderUID: f0.UID, folderTitle: f0.Title}},
},
}
for _, tc := range testCases {
- for featureFlags := range tc.expectedResult {
- t.Run(fmt.Sprintf("%s with featureFlags: %v", tc.desc, featureFlags), func(t *testing.T) {
- dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(featureFlags), tagimpl.ProvideService(sqlStore))
- require.NoError(t, err)
- res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{
- SignedInUser: user,
- Type: tc.typ,
- FolderUIDs: tc.folderUIDs,
- })
- require.NoError(t, err)
- require.Equal(t, len(tc.expectedResult[featureFlags]), len(res))
-
- for i, r := range tc.expectedResult[featureFlags] {
- assert.Equal(t, r.title, res[i].Title)
- if r.folderUID != "" {
- assert.Equal(t, r.folderUID, res[i].FolderUID)
- } else {
- assert.Empty(t, res[i].FolderUID)
- }
- if r.folderTitle != "" {
- assert.Equal(t, r.folderTitle, res[i].FolderTitle)
- } else {
- assert.Empty(t, res[i].FolderTitle)
- }
- }
+ t.Run(tc.desc, func(t *testing.T) {
+ dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore))
+ require.NoError(t, err)
+ res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{
+ SignedInUser: user,
+ Type: tc.typ,
+ FolderUIDs: tc.folderUIDs,
})
- }
+ require.NoError(t, err)
+ require.Equal(t, len(tc.expectedResult), len(res))
+
+ for i, r := range tc.expectedResult {
+ assert.Equal(t, r.title, res[i].Title)
+ if r.folderUID != "" {
+ assert.Equal(t, r.folderUID, res[i].FolderUID)
+ } else {
+ assert.Empty(t, res[i].FolderUID)
+ }
+ if r.folderTitle != "" {
+ assert.Equal(t, r.folderTitle, res[i].FolderTitle)
+ } else {
+ assert.Empty(t, res[i].FolderTitle)
+ }
+ }
+ })
}
}
@@ -1260,6 +1241,34 @@ func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title st
return dash
}
+func insertTestDashFolder(t *testing.T, dashboardStore dashboards.Store, folderStore folder.Store, title string, orgId int64,
+ folderId int64, folderUID string, tags ...interface{}) *dashboards.Dashboard {
+ t.Helper()
+ cmd := dashboards.SaveDashboardCommand{
+ OrgID: orgId,
+ FolderID: folderId, // nolint:staticcheck
+ FolderUID: folderUID,
+ IsFolder: true,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "id": nil,
+ "title": title,
+ "tags": tags,
+ }),
+ }
+ dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
+ require.NoError(t, err)
+ require.NotNil(t, dash)
+ dash.Data.Set("id", dash.ID)
+ dash.Data.Set("uid", dash.UID)
+ _, err = folderStore.Create(context.Background(), folder.CreateFolderCommand{
+ Title: title,
+ UID: dash.UID,
+ OrgID: orgId,
+ })
+ require.NoError(t, err)
+ return dash
+}
+
func insertTestDashboardForPlugin(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64,
folderUID string, isFolder bool, pluginId string) *dashboards.Dashboard {
t.Helper()
diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go
index 1eb611fa670..5ab6e38899c 100644
--- a/pkg/services/dashboards/service/dashboard_service.go
+++ b/pkg/services/dashboards/service/dashboard_service.go
@@ -1162,7 +1162,7 @@ func (dr *DashboardServiceImpl) SetDefaultPermissionsAfterCreate(ctx context.Con
isNested := obj.GetFolder() != ""
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) {
// legacy behavior
- if !isNested || !dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
+ if !isNested {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
@@ -1392,7 +1392,7 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb
ctx, span := tracer.Start(ctx, "dashboards.service.FindDashboards")
defer span.End()
- if dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && len(query.FolderUIDs) > 0 && slices.Contains(query.FolderUIDs, folder.SharedWithMeFolderUID) {
+ if len(query.FolderUIDs) > 0 && slices.Contains(query.FolderUIDs, folder.SharedWithMeFolderUID) {
start := time.Now()
userDashboardUIDs, err := dr.getUserSharedDashboardUIDs(ctx, query.SignedInUser)
if err != nil {
diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go
index 1cda81c5288..f768bdc8128 100644
--- a/pkg/services/dashboards/service/dashboard_service_test.go
+++ b/pkg/services/dashboards/service/dashboard_service_test.go
@@ -1402,7 +1402,6 @@ func TestSearchDashboards(t *testing.T) {
t.Run("Should handle Shared with me folder correctly", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
- service.features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Search", mock.Anything, int64(1), mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
if len(req.Options.Fields) == 0 {
@@ -2095,9 +2094,9 @@ func TestSetDefaultPermissionsAfterCreate(t *testing.T) {
// Setup mocks and service
dashboardStore := &dashboards.FakeDashboardStore{}
folderStore := foldertest.FakeFolderStore{}
- features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
+ features := featuremgmt.WithFeatures()
if tc.featureKubernetesDashboards {
- features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesDashboards, featuremgmt.FlagNestedFolders)
+ features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesDashboards)
}
permService := acmock.NewMockedPermissionsService()
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 10567d153fd..c36496ca97e 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -118,13 +118,6 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaSearchAndStorageSquad,
},
- {
- Name: "nestedFolders",
- Description: "Enable folder nesting",
- Stage: FeatureStageGeneralAvailability,
- Owner: grafanaSearchAndStorageSquad,
- Expression: "true", // enabled by default
- },
{
Name: "alertingBacktesting",
Description: "Rule backtesting API for alerting",
diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv
index 3024aa121f9..767f1dd2539 100644
--- a/pkg/services/featuremgmt/toggles-gitlog.csv
+++ b/pkg/services/featuremgmt/toggles-gitlog.csv
@@ -88,7 +88,6 @@ showDashboardValidationWarnings,2022-10-14T13:51:05Z,,2e16d5499e1cf29e67db5031fe
interFont,2022-10-15T14:22:33Z,2022-12-01T11:59:37Z,9f5e691994c9db15f5984b196ab55fd7213b7f72,Torkel Ödegaard
accessControlOnCall,2022-10-19T16:10:09Z,2025-02-25T12:44:40Z,717bd4a6c051d1447c9e35385a4ba176dd391c65,Gabriel MABILLE
newDBLibrary,2022-10-26T01:20:41Z,2023-11-14T14:51:35Z,a3acfb1a48126119fd4913efbb572de162264e92,Ryan McKinley
-nestedFolders,2022-10-26T14:15:14Z,,b346ae03105af606521bbefec16722d590378646,Kristin Laemmert
datasourceLogger,2022-11-02T13:51:51Z,2023-02-07T11:49:16Z,06705a49e236dc3ab3b35db51b7bad8625e4866f,Carl Bergquist
promQueryBuilder,2022-11-03T17:34:01Z,2022-12-19T13:52:06Z,857e545c5ac71722c8b2d3d27621dfb453dbb8ff,Ryan McKinley
elasticsearchBackendMigration,2022-11-10T15:35:15Z,2023-04-12T12:20:43Z,261d620f1c46eb43282cc444ffb4633b77af4283,Ivana Huckova
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index a3d9818ecd2..9900d133c55 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -13,7 +13,6 @@ grpcServer,preview,@grafana/search-and-storage,false,false,false
cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false
showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false
mysqlAnsiQuotes,experimental,@grafana/search-and-storage,false,false,false
-nestedFolders,GA,@grafana/search-and-storage,false,false,false
alertingBacktesting,experimental,@grafana/alerting-squad,false,false,false
editPanelCSVDragAndDrop,experimental,@grafana/dataviz-squad,false,false,true
logsContextDatasourceUi,GA,@grafana/observability-logs,false,false,true
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index ffca3680409..ae29df4a555 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -63,10 +63,6 @@ const (
// Use double quotes to escape keyword in a MySQL query
FlagMysqlAnsiQuotes = "mysqlAnsiQuotes"
- // FlagNestedFolders
- // Enable folder nesting
- FlagNestedFolders = "nestedFolders"
-
// FlagAlertingBacktesting
// Rule backtesting API for alerting
FlagAlertingBacktesting = "alertingBacktesting"
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 325188cc143..4e18aefb96c 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -2096,19 +2096,6 @@
"codeowner": "@grafana/search-and-storage"
}
},
- {
- "metadata": {
- "name": "nestedFolders",
- "resourceVersion": "1753448760331",
- "creationTimestamp": "2022-10-26T14:15:14Z"
- },
- "spec": {
- "description": "Enable folder nesting",
- "stage": "GA",
- "codeowner": "@grafana/search-and-storage",
- "expression": "true"
- }
- },
{
"metadata": {
"name": "newClickhouseConfigPageDesign",
diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go
index 0bae0f65d6b..62fc24bbd86 100644
--- a/pkg/services/folder/folderimpl/folder.go
+++ b/pkg/services/folder/folderimpl/folder.go
@@ -243,29 +243,11 @@ func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery
}
}
- if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
- qry.WithFullpath = false // do not request full path if nested folders are disabled
- qry.WithFullpathUIDs = false
- }
-
dashFolders, err := s.store.GetFolders(ctx, qry)
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
}
- if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
- if q.WithFullpathUIDs || q.WithFullpath {
- for _, f := range dashFolders { // and fix the full path with folder title (unescaped)
- if q.WithFullpath {
- f.Fullpath = f.Title
- }
- if q.WithFullpathUIDs {
- f.FullpathUIDs = f.UID
- }
- }
- }
- }
-
return dashFolders, nil
}
@@ -286,7 +268,7 @@ func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*fol
return folder.RootFolder, nil
}
- if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && q.UID != nil && *q.UID == folder.SharedWithMeFolderUID {
+ if q.UID != nil && *q.UID == folder.SharedWithMeFolderUID {
return folder.SharedWithMeFolder.WithURL(), nil
}
@@ -317,11 +299,6 @@ func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*fol
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
// nolint:staticcheck
- if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
- f.Fullpath = f.Title // set full path to the folder title (unescaped)
- f.FullpathUIDs = f.UID // set full path to the folder UID
- }
-
return f, err
}
@@ -378,7 +355,7 @@ func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQu
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
- if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && q.UID == folder.SharedWithMeFolderUID {
+ if q.UID == folder.SharedWithMeFolderUID {
return s.GetSharedWithMe(ctx, q, true)
}
@@ -646,7 +623,7 @@ func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*
func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetParentsLegacy")
defer span.End()
- if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) || q.UID == accesscontrol.GeneralFolderUID {
+ if q.UID == accesscontrol.GeneralFolderUID {
return nil, nil
}
if q.UID == folder.SharedWithMeFolderUID {
@@ -667,7 +644,7 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm
dashFolder := dashboards.NewDashboardFolder(cmd.Title)
dashFolder.OrgID = cmd.OrgID
- if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && cmd.ParentUID != "" {
+ if cmd.ParentUID != "" {
// Check that the user is allowed to create a subfolder in this folder
parentUIDScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.ParentUID)
legacyEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, parentUIDScope)
@@ -694,7 +671,7 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm
}
}
- if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && cmd.UID == folder.SharedWithMeFolderUID {
+ if cmd.UID == folder.SharedWithMeFolderUID {
return nil, folder.ErrBadRequest.Errorf("cannot create folder with UID %s", folder.SharedWithMeFolderUID)
}
@@ -819,10 +796,6 @@ func (s *Service) UpdateLegacy(ctx context.Context, cmd *folder.UpdateFolderComm
return nil, err
}
- if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
- return dashFolder, nil
- }
-
// always expose the dashboard store sequential ID
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
// nolint:staticcheck
@@ -1279,17 +1252,15 @@ func (s *Service) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDe
folders := []string{*q.UID}
countsMap := make(folder.DescendantCounts, len(s.registry)+1)
- if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
- descendantFolders, err := s.store.GetDescendants(ctx, q.OrgID, *q.UID)
- if err != nil {
- s.log.ErrorContext(ctx, "failed to get descendant folders", "error", err)
- return nil, err
- }
- for _, f := range descendantFolders {
- folders = append(folders, f.UID)
- }
- countsMap[entity.StandardKindFolder] = int64(len(descendantFolders))
+ descendantFolders, err := s.store.GetDescendants(ctx, q.OrgID, *q.UID)
+ if err != nil {
+ s.log.ErrorContext(ctx, "failed to get descendant folders", "error", err)
+ return nil, err
}
+ for _, f := range descendantFolders {
+ folders = append(folders, f.UID)
+ }
+ countsMap[entity.StandardKindFolder] = int64(len(descendantFolders))
for _, v := range s.registry {
c, err := v.CountInFolders(ctx, q.OrgID, folders, q.SignedInUser)
diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go
index 1e201e60ecf..3d526a4d7f1 100644
--- a/pkg/services/provisioning/dashboards/file_reader_test.go
+++ b/pkg/services/provisioning/dashboards/file_reader_test.go
@@ -129,7 +129,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
}
sql, cfgT := db.InitTestDBWithCfg(t)
- features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
+ features := featuremgmt.WithFeatures()
fStore := folderimpl.ProvideStore(sql)
tagService := tagimpl.ProvideService(sql)
dashStore, err := database.ProvideDashboardStore(sql, cfgT, features, tagService)
diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go
index 02d2d9b2aba..942a81dfbf7 100644
--- a/pkg/services/provisioning/dashboards/validator_test.go
+++ b/pkg/services/provisioning/dashboards/validator_test.go
@@ -48,7 +48,7 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
logger := log.New("test.logger")
sql, cfgT := db.InitTestDBWithCfg(t)
- features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
+ features := featuremgmt.WithFeatures()
fStore := folderimpl.ProvideStore(sql)
tagService := tagimpl.ProvideService(sql)
dashStore, err := database.ProvideDashboardStore(sql, cfgT, features, tagService)
diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go
index b8bf394a16c..2b5a1402b90 100644
--- a/pkg/services/sqlstore/permissions/dashboard.go
+++ b/pkg/services/sqlstore/permissions/dashboard.go
@@ -220,36 +220,23 @@ func (f *accessControlDashboardPermissionFilter) buildClauses(dialect migrator.D
}
permSelector.WriteRune(')')
- switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
- case true:
- if len(permSelectorArgs) > 0 {
- switch f.recursiveQueriesAreSupported {
- case true:
- builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
- recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
- f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
- builder.WriteString(fmt.Sprintf("WHERE d.org_id = ? AND d.uid IN (SELECT uid FROM %s)", recQueryName))
- args = append(args, orgID)
- default:
- nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "folder_id", "d.id", orgID)
- builder.WriteRune('(')
- builder.WriteString(nestedFoldersSelectors)
- args = append(args, nestedFoldersArgs...)
- }
- } else {
+ if len(permSelectorArgs) > 0 {
+ switch f.recursiveQueriesAreSupported {
+ case true:
builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
- builder.WriteString("WHERE 1 = 0")
- }
- default:
- builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
- if len(permSelectorArgs) > 0 {
- builder.WriteString("WHERE d.org_id = ? AND d.uid IN ")
+ recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
+ f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
+ builder.WriteString(fmt.Sprintf("WHERE d.org_id = ? AND d.uid IN (SELECT uid FROM %s)", recQueryName))
args = append(args, orgID)
- builder.WriteString(permSelector.String())
- args = append(args, permSelectorArgs...)
- } else {
- builder.WriteString("WHERE 1 = 0")
+ default:
+ nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "folder_id", "d.id", orgID)
+ builder.WriteRune('(')
+ builder.WriteString(nestedFoldersSelectors)
+ args = append(args, nestedFoldersArgs...)
}
+ } else {
+ builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
+ builder.WriteString("WHERE 1 = 0")
}
builder.WriteString(") AND NOT dashboard.is_folder)")
@@ -297,33 +284,22 @@ func (f *accessControlDashboardPermissionFilter) buildClauses(dialect migrator.D
permSelector.WriteRune(')')
- switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
- case true:
- if len(permSelectorArgs) > 0 {
- switch f.recursiveQueriesAreSupported {
- case true:
- recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
- f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
- builder.WriteString("(dashboard.uid IN ")
- builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
- default:
- nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "d.uid", orgID)
- builder.WriteRune('(')
- builder.WriteString(nestedFoldersSelectors)
- builder.WriteRune(')')
- args = append(args, nestedFoldersArgs...)
- }
- } else {
- builder.WriteString("(1 = 0")
- }
- default:
- if len(permSelectorArgs) > 0 {
+ if len(permSelectorArgs) > 0 {
+ switch f.recursiveQueriesAreSupported {
+ case true:
+ recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
+ f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString("(dashboard.uid IN ")
- builder.WriteString(permSelector.String())
- args = append(args, permSelectorArgs...)
- } else {
- builder.WriteString("(1 = 0")
+ builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
+ default:
+ nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "d.uid", orgID)
+ builder.WriteRune('(')
+ builder.WriteString(nestedFoldersSelectors)
+ builder.WriteRune(')')
+ args = append(args, nestedFoldersArgs...)
}
+ } else {
+ builder.WriteString("(1 = 0")
}
builder.WriteString(" AND dashboard.is_folder)")
} else {
diff --git a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go
index 6fee82f9766..baa8982561a 100644
--- a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go
+++ b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go
@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
- "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@@ -107,36 +106,22 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses(di
permSelector.WriteRune(')')
- switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
- case true:
- if len(permSelectorArgs) > 0 {
- switch f.recursiveQueriesAreSupported {
- case true:
- recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
- f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
- builder.WriteString("(folder.uid IN (SELECT uid FROM " + recQueryName)
- default:
- nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "folder", "uid", "", orgID)
- builder.WriteRune('(')
- builder.WriteString(nestedFoldersSelectors)
- args = append(args, nestedFoldersArgs...)
- }
- f.folderIsRequired = true
- builder.WriteString(") AND NOT dashboard.is_folder)")
- } else {
- builder.WriteString("( 1 = 0 AND NOT dashboard.is_folder)")
+ if len(permSelectorArgs) > 0 {
+ switch f.recursiveQueriesAreSupported {
+ case true:
+ recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
+ f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
+ builder.WriteString("(folder.uid IN (SELECT uid FROM " + recQueryName)
+ default:
+ nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "folder", "uid", "", orgID)
+ builder.WriteRune('(')
+ builder.WriteString(nestedFoldersSelectors)
+ args = append(args, nestedFoldersArgs...)
}
- default:
- builder.WriteString("(")
- if len(permSelectorArgs) > 0 {
- builder.WriteString("folder.uid IN ")
- builder.WriteString(permSelector.String())
- args = append(args, permSelectorArgs...)
- f.folderIsRequired = true
- } else {
- builder.WriteString("1 = 0 ")
- }
- builder.WriteString(" AND NOT dashboard.is_folder)")
+ f.folderIsRequired = true
+ builder.WriteString(") AND NOT dashboard.is_folder)")
+ } else {
+ builder.WriteString("( 1 = 0 AND NOT dashboard.is_folder)")
}
// Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder)
@@ -181,33 +166,22 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses(di
}
permSelector.WriteRune(')')
- switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
- case true:
- if len(permSelectorArgs) > 0 {
- switch f.recursiveQueriesAreSupported {
- case true:
- recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
- f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
- builder.WriteString("(dashboard.uid IN ")
- builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
- default:
- nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "", orgID)
- builder.WriteRune('(')
- builder.WriteString(nestedFoldersSelectors)
- builder.WriteRune(')')
- args = append(args, nestedFoldersArgs...)
- }
- } else {
- builder.WriteString("(1 = 0")
- }
- default:
- if len(permSelectorArgs) > 0 {
+ if len(permSelectorArgs) > 0 {
+ switch f.recursiveQueriesAreSupported {
+ case true:
+ recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
+ f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString("(dashboard.uid IN ")
- builder.WriteString(permSelector.String())
- args = append(args, permSelectorArgs...)
- } else {
- builder.WriteString("(1 = 0")
+ builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
+ default:
+ nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "", orgID)
+ builder.WriteRune('(')
+ builder.WriteString(nestedFoldersSelectors)
+ builder.WriteRune(')')
+ args = append(args, nestedFoldersArgs...)
}
+ } else {
+ builder.WriteString("(1 = 0")
}
builder.WriteString(" AND dashboard.is_folder)")
} else {
diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go
index 716323d3151..8f979a8c4bd 100644
--- a/pkg/services/sqlstore/permissions/dashboard_test.go
+++ b/pkg/services/sqlstore/permissions/dashboard_test.go
@@ -384,14 +384,12 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
permission dashboardaccess.PermissionType
permissions []accesscontrol.Permission
expectedResult []string
- features []any
}{
{
desc: "Should not be able to view dashboards under inherited folders with no permissions if nested folders are enabled",
queryType: searchstore.TypeDashboard,
permission: dashboardaccess.PERMISSION_VIEW,
permissions: nil,
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
@@ -399,14 +397,12 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
permissions: nil,
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
desc: "Should not be able to view inherited dashboards and folders with no permissions if nested folders are enabled",
permission: dashboardaccess.PERMISSION_VIEW,
permissions: nil,
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
@@ -416,7 +412,6 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
},
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"},
},
{
@@ -426,19 +421,8 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"},
},
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"parent", "subfolder"},
},
- {
- desc: "Should not be able to view inherited folders if nested folders are not enabled",
- queryType: searchstore.TypeFolder,
- permission: dashboardaccess.PERMISSION_VIEW,
- permissions: []accesscontrol.Permission{
- {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"},
- },
- features: []any{},
- expectedResult: []string{"parent"},
- },
}
var orgID int64 = 1
@@ -452,7 +436,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.permissions)}}
- for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
+ for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -494,14 +478,12 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
permission dashboardaccess.PermissionType
signedInUserPermissions []accesscontrol.Permission
expectedResult []string
- features []any
}{
{
desc: "Should not be able to view dashboards under inherited folders with no permissions if nested folders are enabled",
queryType: searchstore.TypeDashboard,
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
@@ -509,14 +491,12 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
desc: "Should not be able to view inherited dashboards and folders with no permissions if nested folders are enabled",
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
@@ -526,7 +506,6 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
},
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"},
},
{
@@ -536,19 +515,8 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
},
- features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"parent", "subfolder"},
},
- {
- desc: "Should not be able to view inherited folders if nested folders are not enabled",
- queryType: searchstore.TypeFolder,
- permission: dashboardaccess.PERMISSION_VIEW,
- signedInUserPermissions: []accesscontrol.Permission{
- {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
- },
- features: []any{},
- expectedResult: []string{"parent"},
- },
}
var orgID int64 = 1
@@ -566,7 +534,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
}),
},
}
- for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
+ for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -676,7 +644,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
Scope: "folders:uid:unrelated"})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.signedInUserPermissions)}}
- for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
+ for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -711,56 +679,65 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB {
t.Helper()
- store := db.InitTestDB(t)
- err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
- dashes := make([]dashboards.Dashboard, 0, numFolders+numDashboards)
- for i := 1; i <= numFolders; i++ {
- str := strconv.Itoa(i)
- dashes = append(dashes, dashboards.Dashboard{
- OrgID: 1,
- Slug: str,
- UID: str,
- Title: str,
- IsFolder: true,
- Data: simplejson.New(),
- Created: time.Now(),
- Updated: time.Now(),
- })
- }
- // Seed dashboards
- for i := numFolders + 1; i <= numFolders+numDashboards; i++ {
- str := strconv.Itoa(i)
- folderID := 0
- if i%(numFolders+1) != 0 {
- folderID = i % (numFolders + 1)
- }
- dashes = append(dashes, dashboards.Dashboard{
- OrgID: 1,
- IsFolder: false,
- FolderUID: strconv.Itoa(folderID),
- UID: str,
- Slug: str,
- Title: str,
- Data: simplejson.New(),
- Created: time.Now(),
- Updated: time.Now(),
- })
+ db, cfg := db.InitTestDBWithCfg(t)
+ dashStore, err := database.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db))
+ require.NoError(t, err)
+ fStore := folderimpl.ProvideStore(db)
+
+ // Create a signed-in user for folder creation
+ usr := &user.SignedInUser{
+ UserID: 1,
+ OrgID: 1,
+ Login: "test",
+ }
+
+ // folders need to be created in both the folder and dashboard table
+ for i := 1; i <= numFolders; i++ {
+ str := strconv.Itoa(i)
+ folder, err := fStore.Create(context.Background(), folder.CreateFolderCommand{
+ Title: str,
+ OrgID: 1,
+ UID: str,
+ SignedInUser: usr,
+ })
+ require.NoError(t, err)
+ _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
+ OrgID: 1,
+ Dashboard: simplejson.NewFromAny(map[string]any{
+ "title": str,
+ "uid": folder.UID,
+ }),
+ IsFolder: true,
+ })
+ require.NoError(t, err)
+ }
+
+ // now create dashboards
+ for i := numFolders + 1; i <= numFolders+numDashboards; i++ {
+ str := strconv.Itoa(i)
+ folderID := 0
+ if i%(numFolders+1) != 0 {
+ folderID = i % (numFolders + 1)
}
- // Insert dashboards in batches
- batchSize := 500
- for i := 0; i < len(dashes); i += batchSize {
- end := i + batchSize
- if end > len(dashes) {
- end = len(dashes)
- }
-
- _, err := sess.InsertMulti(dashes[i:end])
- if err != nil {
- return err
- }
+ cmd := dashboards.SaveDashboardCommand{
+ OrgID: 1,
+ Dashboard: simplejson.NewFromAny(map[string]any{
+ "title": str,
+ "uid": str,
+ }),
+ IsFolder: false,
+ }
+ if folderID != 0 {
+ cmd.FolderUID = strconv.Itoa(folderID)
}
+ _, err := dashStore.SaveDashboard(context.Background(), cmd)
+ require.NoError(t, err)
+ }
+
+ // insert permissions
+ err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
role := &accesscontrol.Role{
OrgID: 0,
UID: "basic_viewer",
@@ -792,6 +769,7 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access
}
if len(permissions) > 0 {
+ batchSize := 500
for i := 0; i < len(permissions); i += batchSize {
end := i + batchSize
if end > len(permissions) {
@@ -808,7 +786,7 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access
return nil
})
require.NoError(t, err)
- return store
+ return db
}
func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol.Permission, orgID int64, features featuremgmt.FeatureToggles) db.DB {
diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go
index 4476d60cdf2..8f047d00a49 100644
--- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go
+++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go
@@ -44,11 +44,6 @@ func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards, n
}}
features := featuremgmt.WithFeatures()
- // if nestingLevel > 0 enable nested folders
- if nestingLevel > 0 {
- features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
- }
-
store := setupBenchMark(b, usr, features, numUsers, numDashboards, numFolders, nestingLevel)
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
diff --git a/pkg/services/sqlstore/searchstore/builder.go b/pkg/services/sqlstore/searchstore/builder.go
index ec7f6ca3f8a..f60f0303aad 100644
--- a/pkg/services/sqlstore/searchstore/builder.go
+++ b/pkg/services/sqlstore/searchstore/builder.go
@@ -37,14 +37,9 @@ func (b *Builder) ToSQL(limit, page int64) (string, []any) {
INNER JOIN dashboard ON ids.id = dashboard.id`)
b.sql.WriteString("\n")
- if b.Features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
- // covered by UQE_folder_org_id_uid
- b.sql.WriteString(
- `LEFT OUTER JOIN folder ON folder.uid = dashboard.folder_uid AND folder.org_id = dashboard.org_id`)
- } else {
- b.sql.WriteString(`
- LEFT OUTER JOIN dashboard AS folder ON folder.id = dashboard.folder_id`)
- }
+ // covered by UQE_folder_org_id_uid
+ b.sql.WriteString(
+ `LEFT OUTER JOIN folder ON folder.uid = dashboard.folder_uid AND folder.org_id = dashboard.org_id`)
b.sql.WriteString(`
LEFT OUTER JOIN dashboard_tag ON dashboard.id = dashboard_tag.dashboard_id`)
b.sql.WriteString("\n")
@@ -69,17 +64,9 @@ func (b *Builder) buildSelect() {
dashboard.folder_id,
dashboard.deleted,
folder.uid AS folder_uid,
+ folder.title AS folder_slug,
+ folder.title AS folder_title
`)
- if b.Features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
- b.sql.WriteString(`
- folder.title AS folder_slug,`)
- } else {
- b.sql.WriteString(`
- folder.slug AS folder_slug,`)
- }
- b.sql.WriteString(`
- folder.title AS folder_title `)
-
for _, f := range b.Filters {
if f, ok := f.(model.FilterSelect); ok {
b.sql.WriteString(fmt.Sprintf(", %s", f.Select()))
diff --git a/pkg/services/sqlstore/searchstore/filters.go b/pkg/services/sqlstore/searchstore/filters.go
index f5487ea4bf2..065f4eccea8 100644
--- a/pkg/services/sqlstore/searchstore/filters.go
+++ b/pkg/services/sqlstore/searchstore/filters.go
@@ -66,10 +66,9 @@ func (f FolderFilter) Where() (string, []any) {
}
type FolderUIDFilter struct {
- Dialect migrator.Dialect
- OrgID int64
- UIDs []string
- NestedFoldersEnabled bool
+ Dialect migrator.Dialect
+ OrgID int64
+ UIDs []string
}
func (f FolderUIDFilter) Where() (string, []any) {
@@ -92,35 +91,19 @@ func (f FolderUIDFilter) Where() (string, []any) {
case len(params) < 1:
// do nothing
case len(params) == 1:
- q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)"
- if f.NestedFoldersEnabled {
- q = "dashboard.org_id = ? AND dashboard.folder_uid = ?"
- }
+ q = "dashboard.org_id = ? AND dashboard.folder_uid = ?"
params = append([]any{f.OrgID}, params...)
default:
sqlArray := "(?" + strings.Repeat(",?", len(params)-1) + ")"
- q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN " + sqlArray + ")"
- if f.NestedFoldersEnabled {
- q = "dashboard.org_id = ? AND dashboard.folder_uid IN " + sqlArray
- }
+ q = "dashboard.org_id = ? AND dashboard.folder_uid IN " + sqlArray
params = append([]any{f.OrgID}, params...)
}
if includeGeneral {
if q == "" {
- if f.NestedFoldersEnabled {
- q = "dashboard.folder_uid IS NULL "
- } else {
- q = "dashboard.folder_id = ? "
- params = append(params, 0)
- }
+ q = "dashboard.folder_uid IS NULL "
} else {
- if f.NestedFoldersEnabled {
- q = "(" + q + " OR dashboard.folder_uid IS NULL)"
- } else {
- q = "(" + q + " OR dashboard.folder_id = ?)"
- params = append(params, 0)
- }
+ q = "(" + q + " OR dashboard.folder_uid IS NULL)"
}
}
diff --git a/pkg/services/sqlstore/searchstore/filters_test.go b/pkg/services/sqlstore/searchstore/filters_test.go
index 8c94b95c5e2..41bcdfa8194 100644
--- a/pkg/services/sqlstore/searchstore/filters_test.go
+++ b/pkg/services/sqlstore/searchstore/filters_test.go
@@ -12,67 +12,34 @@ func TestIntegrationFolderUIDFilter(t *testing.T) {
t.Skip("skipping integration test in short mode")
}
testCases := []struct {
- description string
- uids []string
- expectedSql string
- expectedParams []any
- nestedFoldersEnabled bool
+ description string
+ uids []string
+ expectedSql string
+ expectedParams []any
}{
{
- description: "searching general folder",
- uids: []string{"general"},
- expectedSql: "dashboard.folder_id = ? ",
- expectedParams: []any{0},
- nestedFoldersEnabled: false,
+ description: "searching general folder",
+ uids: []string{"general"},
+ expectedSql: "dashboard.folder_uid IS NULL ",
+ expectedParams: []any{},
},
{
- description: "searching a specific folder",
- uids: []string{"abc-123"},
- expectedSql: "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)",
- expectedParams: []any{int64(1), "abc-123"},
- nestedFoldersEnabled: false,
+ description: "searching a specific folder",
+ uids: []string{"abc-123"},
+ expectedSql: "dashboard.org_id = ? AND dashboard.folder_uid = ?",
+ expectedParams: []any{int64(1), "abc-123"},
},
{
- description: "searching a specific folders",
- uids: []string{"abc-123", "def-456"},
- expectedSql: "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (?,?))",
- expectedParams: []any{int64(1), "abc-123", "def-456"},
- nestedFoldersEnabled: false,
+ description: "searching a specific folders",
+ uids: []string{"abc-123", "def-456"},
+ expectedSql: "dashboard.org_id = ? AND dashboard.folder_uid IN (?,?)",
+ expectedParams: []any{int64(1), "abc-123", "def-456"},
},
{
- description: "searching a specific folders or general",
- uids: []string{"general", "abc-123", "def-456"},
- expectedSql: "(dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (?,?)) OR dashboard.folder_id = ?)",
- expectedParams: []any{int64(1), "abc-123", "def-456", 0},
- nestedFoldersEnabled: false,
- },
- {
- description: "searching general folder with nestedFoldersEnabled",
- uids: []string{"general"},
- expectedSql: "dashboard.folder_uid IS NULL ",
- expectedParams: []any{},
- nestedFoldersEnabled: true,
- },
- {
- description: "searching a specific folder with nestedFoldersEnabled",
- uids: []string{"abc-123"},
- expectedSql: "dashboard.org_id = ? AND dashboard.folder_uid = ?",
- expectedParams: []any{int64(1), "abc-123"},
- nestedFoldersEnabled: true,
- },
- {
- description: "searching a specific folders with nestedFoldersEnabled",
- uids: []string{"abc-123", "def-456"},
- expectedSql: "dashboard.org_id = ? AND dashboard.folder_uid IN (?,?)",
- expectedParams: []any{int64(1), "abc-123", "def-456"},
- nestedFoldersEnabled: true,
- },
- {
- description: "searching a specific folders or general with nestedFoldersEnabled",
- uids: []string{"general", "abc-123", "def-456"},
- expectedSql: "(dashboard.org_id = ? AND dashboard.folder_uid IN (?,?) OR dashboard.folder_uid IS NULL)",
- expectedParams: []any{int64(1), "abc-123", "def-456"},
- nestedFoldersEnabled: true,
+ description: "searching a specific folders or general",
+ uids: []string{"general", "abc-123", "def-456"},
+ expectedSql: "(dashboard.org_id = ? AND dashboard.folder_uid IN (?,?) OR dashboard.folder_uid IS NULL)",
+ expectedParams: []any{int64(1), "abc-123", "def-456"},
},
}
@@ -81,10 +48,9 @@ func TestIntegrationFolderUIDFilter(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
f := searchstore.FolderUIDFilter{
- Dialect: store.GetDialect(),
- OrgID: 1,
- UIDs: tc.uids,
- NestedFoldersEnabled: tc.nestedFoldersEnabled,
+ Dialect: store.GetDialect(),
+ OrgID: 1,
+ UIDs: tc.uids,
}
sql, params := f.Where()
diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go
index 935ab7cf2be..bf1d5a2525a 100644
--- a/pkg/services/sqlstore/searchstore/search_test.go
+++ b/pkg/services/sqlstore/searchstore/search_test.go
@@ -146,48 +146,6 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
int64(1),
},
},
- {
- desc: "user with view permission",
- userPermissions: []accesscontrol.Permission{
- {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
- },
- level: dashboardaccess.PERMISSION_VIEW,
- features: featuremgmt.WithFeatures(),
- expectedParams: []any{
- int64(1),
- int64(1),
- int64(1),
- 0,
- "Viewer",
- int64(1),
- 0,
- "dashboards:read",
- "dashboards:view",
- "dashboards:edit",
- "dashboards:admin",
- int64(1),
- int64(1),
- int64(1),
- 0,
- "Viewer",
- int64(1),
- 0,
- "dashboards:read",
- "folders:view",
- "folders:edit",
- "folders:admin",
- int64(1),
- int64(1),
- 0,
- "Viewer",
- int64(1),
- 0,
- "folders:read",
- "folders:view",
- "folders:edit",
- "folders:admin",
- },
- },
{
desc: "user with write permission",
userPermissions: []accesscontrol.Permission{
@@ -196,16 +154,6 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
level: dashboardaccess.PERMISSION_EDIT,
features: featuremgmt.WithFeatures(),
expectedParams: []any{
- int64(1),
- int64(1),
- int64(1),
- 0,
- "Viewer",
- int64(1),
- 0,
- "dashboards:write",
- "dashboards:edit",
- "dashboards:admin",
int64(1),
int64(1),
int64(1),
@@ -218,6 +166,7 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
"folders:admin",
int64(1),
int64(1),
+ int64(1),
0,
"Viewer",
int64(1),
@@ -225,15 +174,26 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
"dashboards:create",
"folders:edit",
"folders:admin",
+ int64(1),
+ int64(1),
+ int64(1),
+ 0,
+ "Viewer",
+ int64(1),
+ 0,
+ "dashboards:write",
+ "dashboards:edit",
+ "dashboards:admin",
+ int64(1),
},
},
{
- desc: "user with view permission with nesting",
+ desc: "user with view permission",
userPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
},
level: dashboardaccess.PERMISSION_VIEW,
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
+ features: featuremgmt.WithFeatures(),
expectedParams: []any{
int64(1),
int64(1),
@@ -272,53 +232,12 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
},
},
{
- desc: "user with view permission with remove subquery",
- userPermissions: []accesscontrol.Permission{
- {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
- },
- level: dashboardaccess.PERMISSION_VIEW,
- features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
- expectedParams: []any{
- int64(1),
- int64(1),
- int64(1),
- 0,
- "Viewer",
- int64(1),
- 0,
- "dashboards:read",
- "dashboards:view",
- "dashboards:edit",
- "dashboards:admin",
- int64(1),
- int64(1),
- 0,
- "Viewer",
- int64(1),
- 0,
- "dashboards:read",
- "folders:view",
- "folders:edit",
- "folders:admin",
- int64(1),
- int64(1),
- 0,
- "Viewer",
- int64(1),
- 0,
- "folders:read",
- "folders:view",
- "folders:edit",
- "folders:admin",
- },
- },
- {
- desc: "user with edit permission with nesting and remove subquery",
+ desc: "user with edit permission remove subquery",
userPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:1"},
},
level: dashboardaccess.PERMISSION_EDIT,
- features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
+ features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
expectedParams: []any{
int64(1),
int64(1),
diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go
index 7db2472ca15..da9f0c3441b 100644
--- a/pkg/tests/api/alerting/api_ruler_test.go
+++ b/pkg/tests/api/alerting/api_ruler_test.go
@@ -361,7 +361,6 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
- EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
diff --git a/pkg/tests/api/folders/api_folder_test.go b/pkg/tests/api/folders/api_folder_test.go
index 156097bcda8..1630c316348 100644
--- a/pkg/tests/api/folders/api_folder_test.go
+++ b/pkg/tests/api/folders/api_folder_test.go
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana-openapi-client-go/client/folders"
"github.com/grafana/grafana-openapi-client-go/models"
- "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests"
@@ -46,7 +45,6 @@ func TestIntegrationFolderServiceGetFolder(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
- EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
})
grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, p)
@@ -199,7 +197,7 @@ func TestIntegrationCreateFolder(t *testing.T) {
})
}
-func TestIntegrationNestedFoldersOn(t *testing.T) {
+func TestIntegrationNestedFolders(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -359,7 +357,6 @@ func TestIntegrationSharedWithMe(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
- EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
@@ -550,7 +547,6 @@ func TestIntegrationFineGrainedPermissions(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
- EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go
index 98b0509df10..80945a51f85 100644
--- a/pkg/tests/apis/folder/folders_test.go
+++ b/pkg/tests/apis/folder/folders_test.go
@@ -150,9 +150,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
DualWriterMode: modeDw,
},
},
- EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
- },
}))
})
@@ -166,9 +163,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
DualWriterMode: modeDw,
},
},
- EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
- },
}))
})
@@ -182,9 +176,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
DualWriterMode: modeDw,
},
},
- EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
- },
}))
})
@@ -198,9 +189,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
DualWriterMode: modeDw,
},
},
- EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
- },
}))
})
}
@@ -228,9 +216,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
},
// We set it to 1 here, so we always get forced pagination based on the response size.
UnifiedStorageMaxPageSizeBytes: 1,
- EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
- },
}), mode)
})
}
@@ -669,9 +654,6 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) {
DualWriterMode: modeDw,
},
},
- EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
- },
})
user := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions)
@@ -776,9 +758,6 @@ func TestIntegrationFolderGetPermissions(t *testing.T) {
DualWriterMode: modeDw,
},
},
- EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
- },
})
// Create parent folder
@@ -959,9 +938,6 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) {
DualWriterMode: modeDw,
},
},
- EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
- },
})
userTest := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions)
@@ -1134,7 +1110,6 @@ func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) {
},
},
EnableFeatureToggles: []string{
- featuremgmt.FlagNestedFolders,
featuremgmt.FlagUnifiedStorageSearch,
},
})
diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx
index 38674034a0d..343dca01335 100644
--- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx
+++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx
@@ -1,6 +1,6 @@
import { fireEvent, render, screen } from 'test/test-utils';
-import { config, setBackendSrv } from '@grafana/runtime';
+import { setBackendSrv } from '@grafana/runtime';
import { setupMockServer } from '@grafana/test-utils/server';
import { getFolderFixtures } from '@grafana/test-utils/unstable';
import { backendSrv } from 'app/core/services/backend_srv';
@@ -144,112 +144,61 @@ describe('NestedFolderPicker', () => {
expect(screen.getByLabelText(folderC.item.title)).toBeInTheDocument();
});
- describe('when nestedFolders is enabled', () => {
- let originalToggles = { ...config.featureToggles };
+ it('can expand and collapse a folder to show its children', async () => {
+ const { user } = render();
- beforeAll(() => {
- config.featureToggles.nestedFolders = true;
- });
+ // Open the picker and wait for children to load
+ const button = await screen.findByRole('button', { name: 'Select folder' });
+ await user.click(button);
+ await screen.findByLabelText(folderA.item.title);
- afterAll(() => {
- config.featureToggles = originalToggles;
- });
+ // Expand Folder A
+ // Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
+ fireEvent.mouseDown(screen.getByRole('button', { name: `Expand folder ${folderA.item.title}` }));
- it('can expand and collapse a folder to show its children', async () => {
- const { user } = render();
+ // Folder A's children are visible
+ expect(await screen.findByLabelText(folderA_folderA.item.title)).toBeInTheDocument();
+ expect(await screen.findByLabelText(folderA_folderB.item.title)).toBeInTheDocument();
- // Open the picker and wait for children to load
- const button = await screen.findByRole('button', { name: 'Select folder' });
- await user.click(button);
- await screen.findByLabelText(folderA.item.title);
+ // Collapse Folder A
+ // Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
+ fireEvent.mouseDown(screen.getByRole('button', { name: `Collapse folder ${folderA.item.title}` }));
+ expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument();
+ expect(screen.queryByLabelText(folderA_folderB.item.title)).not.toBeInTheDocument();
- // Expand Folder A
- // Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
- fireEvent.mouseDown(screen.getByRole('button', { name: `Expand folder ${folderA.item.title}` }));
+ // Expand Folder A again
+ // Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
+ fireEvent.mouseDown(screen.getByRole('button', { name: `Expand folder ${folderA.item.title}` }));
- // Folder A's children are visible
- expect(await screen.findByLabelText(folderA_folderA.item.title)).toBeInTheDocument();
- expect(await screen.findByLabelText(folderA_folderB.item.title)).toBeInTheDocument();
-
- // Collapse Folder A
- // Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
- fireEvent.mouseDown(screen.getByRole('button', { name: `Collapse folder ${folderA.item.title}` }));
- expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument();
- expect(screen.queryByLabelText(folderA_folderB.item.title)).not.toBeInTheDocument();
-
- // Expand Folder A again
- // Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
- fireEvent.mouseDown(screen.getByRole('button', { name: `Expand folder ${folderA.item.title}` }));
-
- // Select the first child
- await user.click(screen.getByLabelText(folderA_folderA.item.title));
- expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title);
- });
-
- it('can expand and collapse a folder to show its children with the keyboard', async () => {
- const { user } = render();
- const button = await screen.findByRole('button', { name: 'Select folder' });
-
- await user.click(button);
-
- // Expand Folder A
- await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowRight}');
-
- // Folder A's children are visible
- expect(await screen.findByLabelText(folderA_folderA.item.title)).toBeInTheDocument();
- expect(await screen.findByLabelText(folderA_folderB.item.title)).toBeInTheDocument();
- expect(await screen.findByLabelText(folderA_folderC.item.title)).toBeInTheDocument();
-
- // Collapse Folder A
- await user.keyboard('{ArrowLeft}');
- expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument();
- expect(screen.queryByLabelText(folderA_folderB.item.title)).not.toBeInTheDocument();
-
- // Expand Folder A again
- await user.keyboard('{ArrowRight}');
-
- // Select the first child
- await user.keyboard('{ArrowDown}{Enter}');
- expect(mockOnChange).toHaveBeenCalledWith(folderA_folderC.item.uid, folderA_folderC.item.title);
- });
+ // Select the first child
+ await user.click(screen.getByLabelText(folderA_folderA.item.title));
+ expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title);
});
- describe('when nestedFolders is disabled', () => {
- let originalToggles = { ...config.featureToggles };
+ it('can expand and collapse a folder to show its children with the keyboard', async () => {
+ const { user } = render();
+ const button = await screen.findByRole('button', { name: 'Select folder' });
- beforeAll(() => {
- config.featureToggles.nestedFolders = false;
- });
+ await user.click(button);
- afterAll(() => {
- config.featureToggles = originalToggles;
- });
+ // Expand Folder A
+ await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowRight}');
- it('does not show an expand button', async () => {
- const { user } = render();
+ // Folder A's children are visible
+ expect(await screen.findByLabelText(folderA_folderA.item.title)).toBeInTheDocument();
+ expect(await screen.findByLabelText(folderA_folderB.item.title)).toBeInTheDocument();
+ expect(await screen.findByLabelText(folderA_folderC.item.title)).toBeInTheDocument();
- // Open the picker and wait for children to load
- const button = await screen.findByRole('button', { name: 'Select folder' });
- await user.click(button);
- await screen.findByLabelText(folderA.item.title);
+ // Collapse Folder A
+ await user.keyboard('{ArrowLeft}');
+ expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument();
+ expect(screen.queryByLabelText(folderA_folderB.item.title)).not.toBeInTheDocument();
- // There should be no expand button
- // Note: we need to use mouseDown here because userEvent's click event doesn't get prevented correctly
- expect(screen.queryByRole('button', { name: `Expand folder ${folderA.item.title}` })).not.toBeInTheDocument();
- });
+ // Expand Folder A again
+ await user.keyboard('{ArrowRight}');
- it('does not expand a folder with the keyboard', async () => {
- const { user } = render();
- const button = await screen.findByRole('button', { name: 'Select folder' });
-
- await user.click(button);
-
- // try to expand Folder A
- await user.keyboard('{ArrowDown}{ArrowDown}{ArrowRight}');
-
- // Folder A's children are not visible
- expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument();
- expect(screen.queryByLabelText(folderA_folderB.item.title)).not.toBeInTheDocument();
- });
+ // Select the first child
+ await user.keyboard('{ArrowDown}{Enter}');
+ expect(mockOnChange).toHaveBeenCalledWith(folderA_folderC.item.uid, folderA_folderC.item.title);
});
});
diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx
index b84eff3cb67..c473ab6bb37 100644
--- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx
+++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx
@@ -7,7 +7,6 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
-import { config } from '@grafana/runtime';
import { Alert, Icon, Input, LoadingBar, Stack, Text, useStyles2 } from '@grafana/ui';
import { getStatusFromError } from 'app/core/utils/errors';
import { useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
@@ -78,8 +77,6 @@ export function NestedFolderPicker({
// but doesn't have access to the folder
const isForbidden = getStatusFromError(selectedFolder.error) === 403;
- const nestedFoldersEnabled = Boolean(config.featureToggles.nestedFolders);
-
const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<(QueryResponse & { items: DashboardViewItem[] }) | null>(null);
const [isFetchingSearchResults, setIsFetchingSearchResults] = useState(false);
@@ -361,7 +358,7 @@ export function NestedFolderPicker({
onFolderExpand={handleFolderExpand}
onFolderSelect={handleFolderSelect}
idPrefix={overlayId}
- foldersAreOpenable={nestedFoldersEnabled && !(search && searchResults)}
+ foldersAreOpenable={!(search && searchResults)}
isItemLoaded={isItemLoaded}
requestLoadMore={handleLoadMore}
/>
diff --git a/public/app/core/components/NestedFolderPicker/useTreeInteractions.ts b/public/app/core/components/NestedFolderPicker/useTreeInteractions.ts
index b1c7bdc8127..30d9d81430f 100644
--- a/public/app/core/components/NestedFolderPicker/useTreeInteractions.ts
+++ b/public/app/core/components/NestedFolderPicker/useTreeInteractions.ts
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import * as React from 'react';
-import { config } from '@grafana/runtime';
import { DashboardsTreeItem } from 'app/features/browse-dashboards/types';
import { DashboardViewItem } from 'app/features/search/types';
@@ -27,7 +26,6 @@ export function useTreeInteractions({
visible,
}: TreeInteractionProps) {
const [focusedItemIndex, setFocusedItemIndex] = useState(-1);
- const nestedFoldersEnabled = Boolean(config.featureToggles.nestedFolders);
useEffect(() => {
if (visible) {
@@ -47,7 +45,7 @@ export function useTreeInteractions({
const handleKeyDown = useCallback(
(ev: React.KeyboardEvent) => {
- const foldersAreOpenable = nestedFoldersEnabled && !search;
+ const foldersAreOpenable = !search;
switch (ev.key) {
// Expand/collapse folder on right/left arrow keys
case 'ArrowRight':
@@ -87,7 +85,7 @@ export function useTreeInteractions({
break;
}
},
- [focusedItemIndex, handleCloseOverlay, handleFolderExpand, handleFolderSelect, nestedFoldersEnabled, search, tree]
+ [focusedItemIndex, handleCloseOverlay, handleFolderExpand, handleFolderSelect, search, tree]
);
return {
diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
index 632904914b1..33e141b05c9 100644
--- a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
+++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
@@ -67,7 +67,7 @@ describe('RuleEditor grafana managed rules', () => {
await user.type(await ui.inputs.name.find(), 'my great new rule');
await user.click(await screen.findByRole('button', { name: /select folder/i }));
- await user.click(await screen.findByLabelText(/folder a/i));
+ await user.click(await screen.findByLabelText('Folder A'));
const groupInput = await ui.inputs.group.find();
await user.click(await byRole('combobox').find(groupInput));
await clickSelectOption(groupInput, grafanaRulerGroup.name);
@@ -148,7 +148,7 @@ describe('RuleEditor grafana managed rules', () => {
await user.type(await ui.inputs.name.find(), 'my great new rule');
await user.click(await screen.findByRole('button', { name: /select folder/i }));
- await user.click(await screen.findByLabelText(/folder a/i));
+ await user.click(await screen.findByLabelText('Folder A'));
// Select the existing group with 5m interval
const groupInput = await ui.inputs.group.find();
diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
index 50334b939e5..19b4ab2880f 100644
--- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
+++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
@@ -202,8 +202,7 @@ export const browseDashboardsAPI = createApi({
};
for (const folderCounts of results) {
- // TODO remove nullish coalescing once nestedFolders is toggled on
- totalCounts.folder += folderCounts.folder ?? 0;
+ totalCounts.folder += folderCounts.folder;
totalCounts.dashboard += folderCounts.dashboard;
totalCounts.alertRule += folderCounts.alertrule;
totalCounts.libraryPanel += folderCounts.librarypanel;
diff --git a/public/app/features/browse-dashboards/api/services.ts b/public/app/features/browse-dashboards/api/services.ts
index a6a511ba0f7..6362b3fb10d 100644
--- a/public/app/features/browse-dashboards/api/services.ts
+++ b/public/app/features/browse-dashboards/api/services.ts
@@ -1,4 +1,4 @@
-import { config, getBackendSrv } from '@grafana/runtime';
+import { getBackendSrv } from '@grafana/runtime';
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
import { NestedFolderDTO } from 'app/features/search/service/types';
@@ -17,7 +17,7 @@ export async function listFolders(
page = 1,
pageSize = PAGE_SIZE
): Promise {
- if (parentUID && !config.featureToggles.nestedFolders) {
+ if (parentUID) {
return [];
}
diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
index 6bc4095ff27..3e6e6b7af19 100644
--- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
+++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
@@ -43,10 +43,7 @@ export function BrowseActions({ folderDTO }: Props) {
);
// Folders can only be moved if nested folders is enabled
- const moveIsInvalid = useMemo(
- () => !config.featureToggles.nestedFolders && Object.values(selectedItems.folder).some((v) => v),
- [selectedItems]
- );
+ const moveIsInvalid = useMemo(() => Object.values(selectedItems.folder).some((v) => v), [selectedItems]);
const isSearching = stateManager.hasSearchFilters();
diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx
index dd019520aeb..24614e1bd93 100644
--- a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx
+++ b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx
@@ -2,7 +2,6 @@ import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestProvider } from 'test/helpers/TestProvider';
-import { config } from '@grafana/runtime';
import { appEvents } from 'app/core/core';
import { ShowModalReactEvent } from 'app/types/events';
@@ -43,225 +42,114 @@ describe('browse-dashboards FolderActionsButton', () => {
jest.restoreAllMocks();
});
- describe('with nestedFolders enabled', () => {
- beforeAll(() => {
- config.featureToggles.nestedFolders = true;
- });
-
- afterAll(() => {
- config.featureToggles.nestedFolders = false;
- });
-
- it('does not render anything when the user has no permissions to do anything', () => {
- jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
- return {
- ...mockPermissions,
- canDeleteFolders: false,
- canEditFolders: false,
- canViewPermissions: false,
- canSetPermissions: false,
- };
- });
- render();
- expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
- });
-
- it('renders a "Folder actions" button when the user has permissions to do something', () => {
- render();
- expect(screen.getByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
- });
-
- it('renders all the options if the user has full permissions', async () => {
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
- });
-
- it('does not render the "Manage permissions" option if the user does not have permission to view permissions', async () => {
- jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
- return {
- ...mockPermissions,
- canViewPermissions: false,
- };
- });
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.queryByRole('menuitem', { name: 'Manage permissions' })).not.toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
- });
-
- it('does not render the "Move" option if the user does not have permission to edit', async () => {
- jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
- return {
- ...mockPermissions,
- canEditFolders: false,
- };
- });
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
- expect(screen.queryByRole('menuitem', { name: 'Move' })).not.toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
- });
-
- it('does not render the "Delete" option if the user does not have permission to delete', async () => {
- jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
- return {
- ...mockPermissions,
- canDeleteFolders: false,
- };
- });
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument();
- expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeInTheDocument();
- });
-
- it('clicking the "Manage permissions" option opens the permissions drawer', async () => {
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- await userEvent.click(screen.getByRole('menuitem', { name: 'Manage permissions' }));
- expect(screen.getByRole('dialog', { name: 'Drawer title Manage permissions' })).toBeInTheDocument();
- });
-
- it('clicking the "Move" option opens the move modal', async () => {
- jest.spyOn(appEvents, 'publish');
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- await userEvent.click(screen.getByRole('menuitem', { name: 'Move' }));
- expect(appEvents.publish).toHaveBeenCalledWith(
- new ShowModalReactEvent(
- expect.objectContaining({
- component: MoveModal,
- })
- )
- );
- });
-
- it('clicking the "Delete" option opens the delete modal', async () => {
- jest.spyOn(appEvents, 'publish');
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
- expect(appEvents.publish).toHaveBeenCalledWith(
- new ShowModalReactEvent(
- expect.objectContaining({
- component: DeleteModal,
- })
- )
- );
+ it('does not render anything when the user has no permissions to do anything', () => {
+ jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
+ return {
+ ...mockPermissions,
+ canDeleteFolders: false,
+ canEditFolders: false,
+ canViewPermissions: false,
+ canSetPermissions: false,
+ };
});
+ render();
+ expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
});
- describe('with nestedFolders disabled', () => {
- it('does not render anything when the user has no permissions to do anything', () => {
- jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
- return {
- ...mockPermissions,
- canDeleteFolders: false,
- canEditFolders: false,
- canViewPermissions: false,
- canSetPermissions: false,
- };
- });
- render();
- expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
+ it('renders a "Folder actions" button when the user has permissions to do something', () => {
+ render();
+ expect(screen.getByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
+ });
+
+ it('renders all the options if the user has full permissions', async () => {
+ render();
+
+ await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
+ expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
+ });
+
+ it('does not render the "Manage permissions" option if the user does not have permission to view permissions', async () => {
+ jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
+ return {
+ ...mockPermissions,
+ canViewPermissions: false,
+ };
});
+ render();
- it('renders a "Folder actions" button when the user has permissions to do something', () => {
- render();
- expect(screen.getByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
+ await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
+ expect(screen.queryByRole('menuitem', { name: 'Manage permissions' })).not.toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
+ });
+
+ it('does not render the "Move" option if the user does not have permission to edit', async () => {
+ jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
+ return {
+ ...mockPermissions,
+ canEditFolders: false,
+ };
});
+ render();
- it('does not render a "Move" button even if it has permissions', async () => {
- render();
+ await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
+ expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
+ expect(screen.queryByRole('menuitem', { name: 'Move' })).not.toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
+ });
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.queryByRole('menuitem', { name: 'Move' })).not.toBeInTheDocument();
+ it('does not render the "Delete" option if the user does not have permission to delete', async () => {
+ jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
+ return {
+ ...mockPermissions,
+ canDeleteFolders: false,
+ };
});
+ render();
- it('renders all the options if the user has full permissions', async () => {
- render();
+ await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
+ expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: 'Move' })).toBeInTheDocument();
+ expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeInTheDocument();
+ });
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
- });
+ it('clicking the "Manage permissions" option opens the permissions drawer', async () => {
+ render();
- it('does not render the "Manage permissions" option if the user does not have permission to view permissions', async () => {
- jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
- return {
- ...mockPermissions,
- canViewPermissions: false,
- };
- });
- render();
+ await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
+ await userEvent.click(screen.getByRole('menuitem', { name: 'Manage permissions' }));
+ expect(screen.getByRole('dialog', { name: 'Drawer title Manage permissions' })).toBeInTheDocument();
+ });
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.queryByRole('menuitem', { name: 'Manage permissions' })).not.toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
- });
+ it('clicking the "Move" option opens the move modal', async () => {
+ jest.spyOn(appEvents, 'publish');
+ render();
- it('does not render the "Move" option if the user does not have permission to edit', async () => {
- jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
- return {
- ...mockPermissions,
- canEditFolders: false,
- };
- });
- render();
+ await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
+ await userEvent.click(screen.getByRole('menuitem', { name: 'Move' }));
+ expect(appEvents.publish).toHaveBeenCalledWith(
+ new ShowModalReactEvent(
+ expect.objectContaining({
+ component: MoveModal,
+ })
+ )
+ );
+ });
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
- expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeInTheDocument();
- });
+ it('clicking the "Delete" option opens the delete modal', async () => {
+ jest.spyOn(appEvents, 'publish');
+ render();
- it('does not render the "Delete" option if the user does not have permission to delete', async () => {
- jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
- return {
- ...mockPermissions,
- canDeleteFolders: false,
- };
- });
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- expect(screen.getByRole('menuitem', { name: 'Manage permissions' })).toBeInTheDocument();
- expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeInTheDocument();
- });
-
- it('clicking the "Manage permissions" option opens the permissions drawer', async () => {
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- await userEvent.click(screen.getByRole('menuitem', { name: 'Manage permissions' }));
- expect(screen.getByRole('dialog', { name: 'Drawer title Manage permissions' })).toBeInTheDocument();
- });
-
- it('clicking the "Delete" option opens the delete modal', async () => {
- jest.spyOn(appEvents, 'publish');
- render();
-
- await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
- await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
- expect(appEvents.publish).toHaveBeenCalledWith(
- new ShowModalReactEvent(
- expect.objectContaining({
- component: DeleteModal,
- })
- )
- );
- });
+ await userEvent.click(screen.getByRole('button', { name: 'Folder actions' }));
+ await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
+ expect(appEvents.publish).toHaveBeenCalledWith(
+ new ShowModalReactEvent(
+ expect.objectContaining({
+ component: DeleteModal,
+ })
+ )
+ );
});
});
diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
index dcb798a9749..4c3df4f4671 100644
--- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
+++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Trans, t } from '@grafana/i18n';
-import { config, locationService, reportInteraction } from '@grafana/runtime';
+import { locationService, reportInteraction } from '@grafana/runtime';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { Permissions } from 'app/core/components/AccessControl';
import { appEvents } from 'app/core/core';
@@ -29,8 +29,8 @@ export function FolderActionsButton({ folder }: Props) {
const { canEditFolders, canDeleteFolders, canViewPermissions, canSetPermissions } = getFolderPermissions(folder);
const isProvisionedFolder = folder.managedBy === ManagerKind.Repo;
- // Can only move folders when nestedFolders is enabled and the folder is not provisioned
- const canMoveFolder = config.featureToggles.nestedFolders && canEditFolders && !isProvisionedFolder;
+ // Can only move folders when the folder is not provisioned
+ const canMoveFolder = canEditFolders && !isProvisionedFolder;
const onMove = async (destinationUID: string) => {
await moveFolder({ folder, destinationUID });
diff --git a/public/app/features/browse-dashboards/permissions.ts b/public/app/features/browse-dashboards/permissions.ts
index ff7839bf2de..ccbba4c6a1b 100644
--- a/public/app/features/browse-dashboards/permissions.ts
+++ b/public/app/features/browse-dashboards/permissions.ts
@@ -1,4 +1,3 @@
-import { config } from '@grafana/runtime';
import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types/accessControl';
import { FolderDTO } from 'app/types/folders';
@@ -8,8 +7,8 @@ function checkFolderPermission(action: AccessControlAction, folderDTO?: FolderDT
}
function checkCanCreateFolders(folderDTO?: FolderDTO) {
- // Can only create a folder if we have permissions and either we're at root or nestedFolders is enabled
- if (folderDTO && folderDTO.uid !== 'general' && !config.featureToggles.nestedFolders) {
+ // Can only create a folder if we have permissions and either we're at root
+ if (folderDTO && folderDTO.uid !== 'general') {
return false;
}
diff --git a/public/app/features/search/service/folders.ts b/public/app/features/search/service/folders.ts
index 90ea27fb616..78c2fec708e 100644
--- a/public/app/features/search/service/folders.ts
+++ b/public/app/features/search/service/folders.ts
@@ -1,4 +1,3 @@
-import config from 'app/core/config';
import { listFolders } from 'app/features/browse-dashboards/api/services';
import { DashboardViewItem } from '../types';
@@ -11,11 +10,6 @@ export async function getFolderChildren(
parentTitle?: string,
dashboardsAtRoot = false
): Promise {
- if (!config.featureToggles.nestedFolders) {
- console.error('getFolderChildren requires nestedFolders feature toggle');
- return [];
- }
-
if (!dashboardsAtRoot && !parentUid) {
// We don't show dashboards at root in folder view yet - they're shown under a dummy 'general'
// folder that FolderView adds in
diff --git a/public/app/types/folders.ts b/public/app/types/folders.ts
index 49450c858c5..5102e1f544d 100644
--- a/public/app/types/folders.ts
+++ b/public/app/types/folders.ts
@@ -44,8 +44,7 @@ export interface FolderState {
}
export interface DescendantCountDTO {
- // TODO: make this required once nestedFolders is enabled by default
- folder?: number;
+ folder: number;
dashboard: number;
librarypanel: number;
alertrule: number;