diff --git a/conf/defaults.ini b/conf/defaults.ini index 121001ca2e6..3caa116d02a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1762,3 +1762,8 @@ hidden_toggles = # Disables updating specific feature toggles in the feature management page read_only_toggles = + +#################################### Public Dashboards ##################################### +[public_dashboards] +# Set to false to disable public dashboards +enabled = true diff --git a/conf/sample.ini b/conf/sample.ini index 4864f270ce9..ad9a28410fd 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1615,3 +1615,9 @@ ;hidden_toggles = # Disable updating specific feature toggles in the feature management page ;read_only_toggles = + +#################################### Public Dashboards ##################################### +[public_dashboards] +# Set to false to disable public dashboards +;enabled = true + diff --git a/docs/sources/dashboards/assess-dashboard-usage/index.md b/docs/sources/dashboards/assess-dashboard-usage/index.md index 5f246daf1f2..4c28d254240 100644 --- a/docs/sources/dashboards/assess-dashboard-usage/index.md +++ b/docs/sources/dashboards/assess-dashboard-usage/index.md @@ -62,7 +62,7 @@ Dashboard insights show the following information: {{% admonition type="note" %}} -If you've enabled the `publicDashboards` feature toggle, you'll also see a Public dashboards tab in your analytics. +If public dashboards are [enabled]({{< relref "../../setup-grafana/configure-grafana/#public_dashboards" >}}), you'll also see a **Public dashboards** tab in your analytics. {{% /admonition %}} diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 9b3ceba46c1..1067b50ea53 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2538,3 +2538,11 @@ Move an app plugin (referenced by its id), including all its pages, to a specifi Move an individual app plugin page (referenced by its `path` field) to a specific navigation section. Format: ` = ` + +## [public_dashboards] + +This section configures the [public dashboards]({{< relref "../../dashboards/dashboard-public" >}}) feature. + +### enabled + +Set this to `false` to disable the public dashboards feature. This prevents users from creating new public dashboards and disables existing ones. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index cfccd488800..cbbd960971a 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -22,7 +22,7 @@ Some features are enabled by default. You can disable these feature by setting t | Feature toggle name | Description | Enabled by default | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | -| `publicDashboards` | Enables public access to dashboards | Yes | +| `publicDashboards` | [Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version. | Yes | | `featureHighlights` | Highlight Grafana Enterprise features | | | `exploreContentOutline` | Content outline sidebar | Yes | | `newVizTooltips` | New visualizations tooltips UX | | diff --git a/docs/sources/setup-grafana/installation/docker/index.md b/docs/sources/setup-grafana/installation/docker/index.md index 40d8fca00dd..1ac0f2042fc 100644 --- a/docs/sources/setup-grafana/installation/docker/index.md +++ b/docs/sources/setup-grafana/installation/docker/index.md @@ -117,10 +117,10 @@ docker run -d -p 3000:3000 --name=grafana \ Grafana supports specifying custom configuration settings using [environment variables]({{< relref "../../../setup-grafana/configure-grafana#override-configuration-with-environment-variables" >}}). ```bash -# enabling public dashboard feature +# enable debug logs docker run -d -p 3000:3000 --name=grafana \ - -e "GF_FEATURE_TOGGLES_ENABLE=publicDashboards" \ + -e "GF_LOG_LEVEL=debug" \ grafana/grafana-enterprise ``` diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 4c9d74d049f..90a1ebbb3d2 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -148,6 +148,7 @@ export interface BootData { */ export interface GrafanaConfig { publicDashboardAccessToken?: string; + publicDashboardsEnabled: boolean; snapshotEnabled: boolean; datasources: { [str: string]: DataSourceInstanceSettings }; panels: { [key: string]: PanelPluginMeta }; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index c0f8fad4b7f..3ab369f4510 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -36,6 +36,7 @@ export type AppPluginConfig = { export class GrafanaBootConfig implements GrafanaConfig { publicDashboardAccessToken?: string; + publicDashboardsEnabled = true; snapshotEnabled = true; datasources: { [str: string]: DataSourceInstanceSettings } = {}; panels: { [key: string]: PanelPluginMeta } = {}; diff --git a/pkg/api/api.go b/pkg/api/api.go index 63a1a04a72f..e3b93836cac 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -161,7 +161,7 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/d-embed", reqSignedIn, middleware.AddAllowEmbeddingHeader(), hs.Index) } - if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) { + if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && hs.Cfg.PublicDashboardsEnabled { // list public dashboards r.Get("/public-dashboards/list", reqSignedIn, hs.Index) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 0a82313ed56..01f47f9e0c1 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -90,9 +90,8 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response err error ) - // If public dashboards is enabled and we have a public dashboard, update meta - // values - if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) { + // If public dashboards is enabled and we have a public dashboard, update meta values + if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && hs.Cfg.PublicDashboardsEnabled { publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.SignedInUser.GetOrgID(), dash.UID) if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) { return response.Error(http.StatusInternalServerError, "Error while retrieving public dashboards", err) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 90cb33a42df..7304de65e48 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -272,7 +272,7 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { pubDashService := publicdashboards.NewFakePublicDashboardService(t) pubDashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil).Maybe() middleware := publicdashboards.NewFakePublicDashboardMiddleware(t) - hs.PublicDashboardsApi = api.ProvideApi(pubDashService, nil, hs.AccessControl, featuremgmt.WithFeatures(), middleware) + hs.PublicDashboardsApi = api.ProvideApi(pubDashService, nil, hs.AccessControl, featuremgmt.WithFeatures(), middleware, hs.Cfg) guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) }) diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 28fb545d26d..5d60f6e320a 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -238,6 +238,7 @@ type FrontendSettingsDTO struct { GeomapDisableCustomBaseLayer bool `json:"geomapDisableCustomBaseLayer"` PublicDashboardAccessToken string `json:"publicDashboardAccessToken"` + PublicDashboardsEnabled bool `json:"publicDashboardsEnabled"` DateFormats setting.DateFormats `json:"dateFormats,omitempty"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index e8146b80abe..262185ec2e4 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -158,6 +158,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro SecureSocksDSProxyEnabled: hs.Cfg.SecureSocksDSProxy.Enabled && hs.Cfg.SecureSocksDSProxy.ShowUI, DisableFrontendSandboxForPlugins: hs.Cfg.DisableFrontendSandboxForPlugins, PublicDashboardAccessToken: c.PublicDashboardAccessToken, + PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled, SharedWithMeFolderUID: folder.SharedWithMeFolderUID, BuildInfo: dtos.FrontendSettingsBuildInfoDTO{ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 95d213bb2ef..2796130d52e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -48,7 +48,7 @@ var ( }, { Name: "publicDashboards", - Description: "Enables public access to dashboards", + Description: "[Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version.", Stage: FeatureStageGeneralAvailability, Owner: grafanaSharingSquad, Expression: "true", // enabled by default diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e386649422c..75cec52aff3 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -24,7 +24,7 @@ const ( FlagPanelTitleSearch = "panelTitleSearch" // FlagPublicDashboards - // Enables public access to dashboards + // [Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version. FlagPublicDashboards = "publicDashboards" // FlagPublicDashboardsEmailSharing diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index e6c7b6c36b6..6015c1ff5b4 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -358,7 +358,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt Icon: "library-panel", }) - if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPublicDashboards) { + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPublicDashboards) && s.cfg.PublicDashboardsEnabled { dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ Text: "Public dashboards", Id: "dashboards/public", diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index 3713e7d8c59..77fa58df291 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -17,16 +17,19 @@ import ( "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) type Api struct { PublicDashboardService publicdashboards.Service - RouteRegister routing.RouteRegister - AccessControl accesscontrol.AccessControl - Features *featuremgmt.FeatureManager - Log log.Logger Middleware publicdashboards.Middleware + + accessControl accesscontrol.AccessControl + cfg *setting.Cfg + features *featuremgmt.FeatureManager + log log.Logger + routeRegister routing.RouteRegister } func ProvideApi( @@ -35,21 +38,27 @@ func ProvideApi( ac accesscontrol.AccessControl, features *featuremgmt.FeatureManager, md publicdashboards.Middleware, + cfg *setting.Cfg, ) *Api { api := &Api{ PublicDashboardService: pd, - RouteRegister: rr, - AccessControl: ac, - Features: features, - Log: log.New("publicdashboards.api"), Middleware: md, + accessControl: ac, + cfg: cfg, + features: features, + log: log.New("publicdashboards.api"), + routeRegister: rr, } - // attach api if PublicDashboards feature flag is enabled - if features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) { + // register endpoints if the feature is enabled + if features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && cfg.PublicDashboardsEnabled { api.RegisterAPIEndpoints() } + if !features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) { + api.log.Warn("[Deprecated] The publicDashboards feature toggle will be removed in Grafana v11. To disable the public dashboards feature, use the public_dashboards.enabled setting.") + } + return api } @@ -59,35 +68,35 @@ func (api *Api) RegisterAPIEndpoints() { // Anonymous access to public dashboard route is configured in pkg/api/api.go // because it is deeply dependent on the HTTPServer.Index() method and would result in a // circular dependency - api.RouteRegister.Group("/api/public/dashboards/:accessToken", func(apiRoute routing.RouteRegister) { + api.routeRegister.Group("/api/public/dashboards/:accessToken", func(apiRoute routing.RouteRegister) { apiRoute.Get("/", routing.Wrap(api.ViewPublicDashboard)) apiRoute.Get("/annotations", routing.Wrap(api.GetPublicAnnotations)) apiRoute.Post("/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard)) }, api.Middleware.HandleApi) // Auth endpoints - auth := accesscontrol.Middleware(api.AccessControl) + auth := accesscontrol.Middleware(api.accessControl) uidScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(accesscontrol.Parameter(":dashboardUid")) // List public dashboards for org - api.RouteRegister.Get("/api/dashboards/public-dashboards", middleware.ReqSignedIn, routing.Wrap(api.ListPublicDashboards)) + api.routeRegister.Get("/api/dashboards/public-dashboards", middleware.ReqSignedIn, routing.Wrap(api.ListPublicDashboards)) // Get public dashboard - api.RouteRegister.Get("/api/dashboards/uid/:dashboardUid/public-dashboards", + api.routeRegister.Get("/api/dashboards/uid/:dashboardUid/public-dashboards", auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, uidScope)), routing.Wrap(api.GetPublicDashboard)) // Create Public Dashboard - api.RouteRegister.Post("/api/dashboards/uid/:dashboardUid/public-dashboards", + api.routeRegister.Post("/api/dashboards/uid/:dashboardUid/public-dashboards", auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)), routing.Wrap(api.CreatePublicDashboard)) // Update Public Dashboard - api.RouteRegister.Patch("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid", + api.routeRegister.Patch("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid", auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)), routing.Wrap(api.UpdatePublicDashboard)) // Delete Public dashboard - api.RouteRegister.Delete("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid", + api.routeRegister.Delete("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid", auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)), routing.Wrap(api.DeletePublicDashboard)) } diff --git a/pkg/services/publicdashboards/api/api_test.go b/pkg/services/publicdashboards/api/api_test.go index 176bacd7aed..662b2cefaeb 100644 --- a/pkg/services/publicdashboards/api/api_test.go +++ b/pkg/services/publicdashboards/api/api_test.go @@ -13,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -27,7 +26,7 @@ var userAdmin = &user.SignedInUser{UserID: 2, OrgID: 1, OrgRole: org.RoleAdmin, var userViewer = &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleViewer, Login: "testViewerUserRBAC", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}}}} var anonymousUser = &user.SignedInUser{IsAnonymous: true} -func TestAPIFeatureFlag(t *testing.T) { +func TestAPIFeatureDisabled(t *testing.T) { testCases := []struct { Name string Method string @@ -71,11 +70,18 @@ func TestAPIFeatureFlag(t *testing.T) { } for _, test := range testCases { - t.Run(test.Name, func(t *testing.T) { + t.Run(test.Name+" - setting disabled", func(t *testing.T) { cfg := setting.NewCfg() + cfg.PublicDashboardsEnabled = false service := publicdashboards.NewFakePublicDashboardService(t) - features := featuremgmt.WithFeatures() - testServer := setupTestServer(t, cfg, features, service, nil, userAdmin) + testServer := setupTestServer(t, cfg, service, userAdmin, true) + response := callAPI(testServer, test.Method, test.Path, nil, t) + assert.Equal(t, http.StatusNotFound, response.Code) + }) + + t.Run(test.Name+" - feature flag disabled", func(t *testing.T) { + service := publicdashboards.NewFakePublicDashboardService(t) + testServer := setupTestServer(t, nil, service, userAdmin, false) response := callAPI(testServer, test.Method, test.Path, nil, t) assert.Equal(t, http.StatusNotFound, response.Code) }) @@ -130,9 +136,7 @@ func TestAPIListPublicDashboard(t *testing.T) { service.On("FindAllWithPagination", mock.Anything, mock.Anything, mock.Anything). Return(test.Response, test.ResponseErr).Maybe() - cfg := setting.NewCfg() - features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards) - testServer := setupTestServer(t, cfg, features, service, nil, test.User) + testServer := setupTestServer(t, nil, service, test.User, true) response := callAPI(testServer, http.MethodGet, "/api/dashboards/public-dashboards", nil, t) assert.Equal(t, test.ExpectedHttpResponse, response.Code) @@ -259,10 +263,7 @@ func TestAPIDeletePublicDashboard(t *testing.T) { Return(test.ResponseErr) } - cfg := setting.NewCfg() - - features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards) - testServer := setupTestServer(t, cfg, features, service, nil, test.User) + testServer := setupTestServer(t, nil, service, test.User, true) response := callAPI(testServer, http.MethodDelete, fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid), nil, t) assert.Equal(t, test.ExpectedHttpResponse, response.Code) @@ -347,16 +348,7 @@ func TestAPIGetPublicDashboard(t *testing.T) { Return(test.PublicDashboardResult, test.PublicDashboardErr) } - cfg := setting.NewCfg() - - testServer := setupTestServer( - t, - cfg, - featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), - service, - nil, - test.User, - ) + testServer := setupTestServer(t, nil, service, test.User, true) response := callAPI( testServer, @@ -474,16 +466,7 @@ func TestApiCreatePublicDashboard(t *testing.T) { Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr) } - cfg := setting.NewCfg() - - testServer := setupTestServer( - t, - cfg, - featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), - service, - nil, - test.User, - ) + testServer := setupTestServer(t, nil, service, test.User, true) response := callAPI( testServer, @@ -609,9 +592,6 @@ func TestAPIUpdatePublicDashboard(t *testing.T) { } for _, test := range testCases { - cfg := setting.NewCfg() - features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards) - t.Run(test.Name, func(t *testing.T) { service := publicdashboards.NewFakePublicDashboardService(t) @@ -620,7 +600,7 @@ func TestAPIUpdatePublicDashboard(t *testing.T) { Return(test.ExpectedResponse, test.ExpectedError) } - testServer := setupTestServer(t, cfg, features, service, nil, test.User) + testServer := setupTestServer(t, nil, service, test.User, true) url := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid) body := strings.NewReader(test.Body) diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go index 3be29466b68..b8dc715b71c 100644 --- a/pkg/services/publicdashboards/api/common_test.go +++ b/pkg/services/publicdashboards/api/common_test.go @@ -40,11 +40,12 @@ import ( func setupTestServer( t *testing.T, cfg *setting.Cfg, - features *featuremgmt.FeatureManager, service publicdashboards.Service, - db db.DB, user *user.SignedInUser, + ffEnabled bool, ) *web.Mux { + t.Helper() + // build router to register routes rr := routing.NewRouteRegister() @@ -56,9 +57,18 @@ func setupTestServer( // set initial context m.Use(contextProvider(&testContext{user})) - // build api, this will mount the routes at the same time if - // featuremgmt.FlagPublicDashboard is enabled - ProvideApi(service, rr, ac, features, &Middleware{}) + features := featuremgmt.WithFeatures() + if ffEnabled { + features = featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards) + } + + if cfg == nil { + cfg = setting.NewCfg() + cfg.PublicDashboardsEnabled = true + } + + // build api, this will mount the routes at the same time if the feature is enabled + ProvideApi(service, rr, ac, features, &Middleware{}, cfg) // connect routes to mux rr.Register(m.Router) diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go index e6dabff13d6..1f43b123412 100644 --- a/pkg/services/publicdashboards/api/query.go +++ b/pkg/services/publicdashboards/api/query.go @@ -72,7 +72,7 @@ func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Respon return response.Err(err) } - return toJsonStreamingResponse(c.Req.Context(), api.Features, resp) + return toJsonStreamingResponse(c.Req.Context(), api.features, resp) } // swagger:route GET /public/dashboards/{accessToken}/annotations dashboard_public getPublicAnnotations diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 2dcaaa75611..d6efc69b738 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -101,16 +101,7 @@ func TestAPIViewPublicDashboard(t *testing.T) { service.On("GetPublicDashboardForView", mock.Anything, mock.AnythingOfType("string")). Return(test.DashboardResult, test.Err).Maybe() - cfg := setting.NewCfg() - - testServer := setupTestServer( - t, - cfg, - featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), - service, - nil, - anonymousUser, - ) + testServer := setupTestServer(t, nil, service, anonymousUser, true) response := callAPI(testServer, http.MethodGet, fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken), @@ -202,16 +193,7 @@ func TestAPIQueryPublicDashboard(t *testing.T) { setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) { service := publicdashboards.NewFakePublicDashboardService(t) - cfg := setting.NewCfg() - - testServer := setupTestServer( - t, - cfg, - featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled), - service, - nil, - anonymousUser, - ) + testServer := setupTestServer(t, nil, service, anonymousUser, true) return testServer, service } @@ -338,6 +320,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) // create public dashboard store := publicdashboardsStore.ProvideStore(db, db.Cfg, featuremgmt.WithFeatures()) cfg := setting.NewCfg() + cfg.PublicDashboardsEnabled = true ac := acmock.New() ws := publicdashboardsService.ProvideServiceWrapper(store) folderStore := folderimpl.ProvideDashboardFolderStore(db) @@ -354,13 +337,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) require.NoError(t, err) // setup test server - server := setupTestServer(t, - cfg, - featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), - pds, - db, - anonymousUser, - ) + server := setupTestServer(t, cfg, pds, anonymousUser, true) resp := callAPI(server, http.MethodPost, fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken), @@ -436,7 +413,6 @@ func TestAPIGetAnnotations(t *testing.T) { } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { - cfg := setting.NewCfg() service := publicdashboards.NewFakePublicDashboardService(t) if test.ExpectedServiceCalled { @@ -444,7 +420,7 @@ func TestAPIGetAnnotations(t *testing.T) { Return(test.Annotations, test.ServiceError).Once() } - testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, anonymousUser) + testServer := setupTestServer(t, nil, service, anonymousUser, true) path := fmt.Sprintf("/api/public/dashboards/%s/annotations?from=%s&to=%s", test.AccessToken, test.From, test.To) response := callAPI(testServer, http.MethodGet, path, nil, t) diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index dcf080f24ef..03f826afa4f 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -58,7 +58,7 @@ func TestIntegrationListPublicDashboard(t *testing.T) { var publicdashboardStore *PublicDashboardStoreImpl setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) + sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{}) quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -448,7 +448,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { var savedDashboard2 *dashboards.Dashboard setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) + sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{}) quotaService := quotatest.New(false, nil) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -528,7 +528,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { var err error setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) + sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{}) quotaService := quotatest.New(false, nil) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) @@ -788,7 +788,7 @@ func TestGetMetrics(t *testing.T) { var savedDashboard4 *dashboards.Dashboard setup := func() { - sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) + sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{}) quotaService := quotatest.New(false, nil) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 126d9c0788a..77678629047 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -541,6 +541,9 @@ type Cfg struct { // sqlstore package and HTTP middlewares. DatabaseInstrumentQueries bool + // Public dashboards + PublicDashboardsEnabled bool + // Feature Management Settings FeatureManagement FeatureMgmtSettings } @@ -1254,6 +1257,7 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { cfg.UserFacingDefaultError = logSection.Key("user_facing_default_error").MustString("please inspect Grafana server log for details") cfg.readFeatureManagementConfig() + cfg.readPublicDashboardsSettings() return nil } @@ -1965,3 +1969,8 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { cfg.LiveAllowedOrigins = originPatterns return nil } + +func (cfg *Cfg) readPublicDashboardsSettings() { + publicDashboards := cfg.Raw.Section("public_dashboards") + cfg.PublicDashboardsEnabled = publicDashboards.Key("enabled").MustBool(true) +} diff --git a/public/app/features/admin/UserListPage.tsx b/public/app/features/admin/UserListPage.tsx index 918a78c99d8..98bf8a200fb 100644 --- a/public/app/features/admin/UserListPage.tsx +++ b/public/app/features/admin/UserListPage.tsx @@ -6,6 +6,7 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { config, featureEnabled } from '@grafana/runtime'; import { useStyles2, TabsBar, Tab } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; +import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { Page } from '../../core/components/Page/Page'; import { AccessControlAction } from '../../types'; @@ -46,7 +47,7 @@ export default function UserListPage() { const hasAccessToAdminUsers = contextSrv.hasPermission(AccessControlAction.UsersRead); const hasAccessToOrgUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); const hasEmailSharingEnabled = - Boolean(config.featureToggles.publicDashboards) && + isPublicDashboardsEnabled() && Boolean(config.featureToggles.publicDashboardsEmailSharing) && featureEnabled('publicDashboardsEmailSharing'); diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index 994b91127e1..acc6ddfcd9f 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -5,6 +5,7 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel, Scene import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { t } from 'app/core/internationalization'; +import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardScene } from '../scene/DashboardScene'; import { DashboardInteractions } from '../utils/interactions'; @@ -61,7 +62,7 @@ export class ShareModal extends SceneObjectBase implements Moda } } - if (Boolean(config.featureToggles['publicDashboards'])) { + if (isPublicDashboardsEnabled()) { tabs.push(new SharePublicDashboardTab({ dashboardRef, modalRef: this.getRef() })); } diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx index 8de11889558..fa00c6c99c4 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx @@ -5,6 +5,7 @@ import { config } from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { t } from 'app/core/internationalization'; import { SharePublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard'; +import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard'; @@ -56,7 +57,7 @@ function getTabs(panel?: PanelModel, activeTab?: string) { tabs.push(...customDashboardTabs); } - if (Boolean(config.featureToggles['publicDashboards'])) { + if (isPublicDashboardsEnabled()) { tabs.push({ label: 'Public dashboard', value: shareDashboardType.publicDashboard, diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert.tsx index b850893291d..4b56dbdd272 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert.tsx @@ -12,6 +12,6 @@ export const NoUpsertPermissionsAlert = ({ mode }: { mode: 'create' | 'edit' }) data-testid={selectors.NoUpsertPermissionsWarningAlert} bottomSpacing={0} > - Contact your admin to get permission to {mode} create public dashboards + Contact your admin to get permission to {mode} public dashboards ); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx index 117e165498d..e5d26f503a8 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx @@ -70,6 +70,7 @@ beforeAll(() => { beforeEach(() => { config.featureToggles.publicDashboards = true; + config.publicDashboardsEnabled = true; jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); jest.spyOn(contextSrv, 'hasRole').mockReturnValue(true); @@ -137,7 +138,14 @@ describe('SharePublic', () => { beforeEach(() => { server.use(getExistentPublicDashboardResponse()); }); - it('does not render share panel when public dashboards feature is disabled', async () => { + it('does not render share panel when public dashboards feature is disabled using config setting', async () => { + config.publicDashboardsEnabled = false; + await renderSharePublicDashboard(undefined, false); + + expect(screen.getByRole('tablist')).toHaveTextContent('Link'); + expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard'); + }); + it('does not render share panel when public dashboards feature is disabled using feature toggle', async () => { config.featureToggles.publicDashboards = false; await renderSharePublicDashboard(undefined, false); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts index cd5ca7c6faf..621fde2b0f6 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts @@ -1,5 +1,5 @@ import { TypedVariableModel } from '@grafana/data'; -import { DataSourceWithBackend } from '@grafana/runtime'; +import { config, DataSourceWithBackend } from '@grafana/runtime'; import { getConfig } from 'app/core/config'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -93,3 +93,7 @@ export const generatePublicDashboardConfigUrl = (dashboardUid: string, dashboard }; export const validEmailRegex = /^[A-Z\d._%+-]+@[A-Z\d.-]+\.[A-Z]{2,}$/i; + +export const isPublicDashboardsEnabled = () => { + return Boolean(config.featureToggles.publicDashboards) && config.publicDashboardsEnabled; +}; diff --git a/public/app/features/dashboard/routes.ts b/public/app/features/dashboard/routes.ts index f76da677f68..1ee8a7a88b4 100644 --- a/public/app/features/dashboard/routes.ts +++ b/public/app/features/dashboard/routes.ts @@ -4,32 +4,33 @@ import { RouteDescriptor } from '../../core/navigation/types'; import { DashboardRoutes } from '../../types'; export const getPublicDashboardRoutes = (): RouteDescriptor[] => { - if (config.featureToggles.publicDashboards) { - return [ - { - path: '/dashboard/public', - pageClass: 'page-dashboard', - routeName: DashboardRoutes.Public, - component: SafeDynamicImport( - () => - import( - /* webpackChunkName: "ListPublicDashboardPage" */ '../../features/manage-dashboards/PublicDashboardListPage' - ) - ), - }, - { - path: '/public-dashboards/:accessToken', - pageClass: 'page-dashboard', - routeName: DashboardRoutes.Public, - chromeless: true, - component: SafeDynamicImport( - () => - import( - /* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPage' - ) - ), - }, - ]; + if (!config.publicDashboardsEnabled || !config.featureToggles.publicDashboards) { + return []; } - return []; + + return [ + { + path: '/dashboard/public', + pageClass: 'page-dashboard', + routeName: DashboardRoutes.Public, + component: SafeDynamicImport( + () => + import( + /* webpackChunkName: "ListPublicDashboardPage" */ '../../features/manage-dashboards/PublicDashboardListPage' + ) + ), + }, + { + path: '/public-dashboards/:accessToken', + pageClass: 'page-dashboard', + routeName: DashboardRoutes.Public, + chromeless: true, + component: SafeDynamicImport( + () => + import( + /* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPage' + ) + ), + }, + ]; };