Nested folders: Remove feature flag (#109212)
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -80,11 +80,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
mysqlAnsiQuotes?: boolean;
|
||||
/**
|
||||
* Enable folder nesting
|
||||
* @default true
|
||||
*/
|
||||
nestedFolders?: boolean;
|
||||
/**
|
||||
* Rule backtesting API for alerting
|
||||
*/
|
||||
alertingBacktesting?: boolean;
|
||||
|
||||
+38
-88
@@ -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
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
+2
-31
@@ -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{}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
||||
|
||||
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(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
||||
// 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(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
||||
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(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
|
||||
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(<NestedFolderPicker onChange={mockOnChange} />);
|
||||
// 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(<NestedFolderPicker onChange={mockOnChange} />);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<DashboardViewItem[]> {
|
||||
if (parentUID && !config.featureToggles.nestedFolders) {
|
||||
if (parentUID) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(<FolderActionsButton folder={mockFolder} />);
|
||||
expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a "Folder actions" button when the user has permissions to do something', () => {
|
||||
render(<FolderActionsButton folder={mockFolder} />);
|
||||
expect(screen.getByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all the options if the user has full permissions', async () => {
|
||||
render(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
|
||||
it('renders a "Folder actions" button when the user has permissions to do something', () => {
|
||||
render(<FolderActionsButton folder={mockFolder} />);
|
||||
expect(screen.getByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all the options if the user has full permissions', async () => {
|
||||
render(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
it('renders a "Folder actions" button when the user has permissions to do something', () => {
|
||||
render(<FolderActionsButton folder={mockFolder} />);
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
it('does not render a "Move" button even if it has permissions', async () => {
|
||||
render(<FolderActionsButton folder={mockFolder} />);
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
it('renders all the options if the user has full permissions', async () => {
|
||||
render(<FolderActionsButton folder={mockFolder} />);
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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(<FolderActionsButton folder={mockFolder} />);
|
||||
|
||||
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,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DashboardViewItem[]> {
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user