PublicDashboards: Add setting to disable the feature (#78894)

* Replace feature toggle with configuration setting

* Fix permission alert

* Update documentation

* Add back feature toggle

* revert unwanted commited changes

* fix tests

* run prettier

* Update SharePublicDashboard.test.tsx

* fix linter and frontend tests

* Update api.go

* Apply docs edit from code review

Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>

* Update index.md

* Update docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* add isPublicDashboardsEnabled + test

* fix test

* update ff description in registry

* move isPublicDashboardsEnabled

* revert getConfig() update

---------

Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
This commit is contained in:
Agnès Toulet 2023-12-19 11:43:54 +01:00 committed by GitHub
parent ef60c90dfa
commit fdaf6e3f2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 159 additions and 137 deletions

View File

@ -1762,3 +1762,8 @@ hidden_toggles =
# Disables updating specific feature toggles in the feature management page # Disables updating specific feature toggles in the feature management page
read_only_toggles = read_only_toggles =
#################################### Public Dashboards #####################################
[public_dashboards]
# Set to false to disable public dashboards
enabled = true

View File

@ -1615,3 +1615,9 @@
;hidden_toggles = ;hidden_toggles =
# Disable updating specific feature toggles in the feature management page # Disable updating specific feature toggles in the feature management page
;read_only_toggles = ;read_only_toggles =
#################################### Public Dashboards #####################################
[public_dashboards]
# Set to false to disable public dashboards
;enabled = true

View File

@ -62,7 +62,7 @@ Dashboard insights show the following information:
{{% admonition type="note" %}} {{% 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 %}} {{% /admonition %}}

View File

@ -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. Move an individual app plugin page (referenced by its `path` field) to a specific navigation section.
Format: `<pageUrl> = <sectionId> <sortWeight>` Format: `<pageUrl> = <sectionId> <sortWeight>`
## [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.

View File

@ -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 | | Feature toggle name | Description | Enabled by default |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | | `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 | | | `featureHighlights` | Highlight Grafana Enterprise features | |
| `exploreContentOutline` | Content outline sidebar | Yes | | `exploreContentOutline` | Content outline sidebar | Yes |
| `newVizTooltips` | New visualizations tooltips UX | | | `newVizTooltips` | New visualizations tooltips UX | |

View File

@ -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" >}}). Grafana supports specifying custom configuration settings using [environment variables]({{< relref "../../../setup-grafana/configure-grafana#override-configuration-with-environment-variables" >}}).
```bash ```bash
# enabling public dashboard feature # enable debug logs
docker run -d -p 3000:3000 --name=grafana \ docker run -d -p 3000:3000 --name=grafana \
-e "GF_FEATURE_TOGGLES_ENABLE=publicDashboards" \ -e "GF_LOG_LEVEL=debug" \
grafana/grafana-enterprise grafana/grafana-enterprise
``` ```

View File

@ -148,6 +148,7 @@ export interface BootData {
*/ */
export interface GrafanaConfig { export interface GrafanaConfig {
publicDashboardAccessToken?: string; publicDashboardAccessToken?: string;
publicDashboardsEnabled: boolean;
snapshotEnabled: boolean; snapshotEnabled: boolean;
datasources: { [str: string]: DataSourceInstanceSettings }; datasources: { [str: string]: DataSourceInstanceSettings };
panels: { [key: string]: PanelPluginMeta }; panels: { [key: string]: PanelPluginMeta };

View File

@ -36,6 +36,7 @@ export type AppPluginConfig = {
export class GrafanaBootConfig implements GrafanaConfig { export class GrafanaBootConfig implements GrafanaConfig {
publicDashboardAccessToken?: string; publicDashboardAccessToken?: string;
publicDashboardsEnabled = true;
snapshotEnabled = true; snapshotEnabled = true;
datasources: { [str: string]: DataSourceInstanceSettings } = {}; datasources: { [str: string]: DataSourceInstanceSettings } = {};
panels: { [key: string]: PanelPluginMeta } = {}; panels: { [key: string]: PanelPluginMeta } = {};

View File

@ -161,7 +161,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/d-embed", reqSignedIn, middleware.AddAllowEmbeddingHeader(), hs.Index) 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 // list public dashboards
r.Get("/public-dashboards/list", reqSignedIn, hs.Index) r.Get("/public-dashboards/list", reqSignedIn, hs.Index)

View File

@ -90,9 +90,8 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
err error err error
) )
// If public dashboards is enabled and we have a public dashboard, update meta // If public dashboards is enabled and we have a public dashboard, update meta values
// values if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && hs.Cfg.PublicDashboardsEnabled {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) {
publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.SignedInUser.GetOrgID(), dash.UID) publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.SignedInUser.GetOrgID(), dash.UID)
if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) { if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) {
return response.Error(http.StatusInternalServerError, "Error while retrieving public dashboards", err) return response.Error(http.StatusInternalServerError, "Error while retrieving public dashboards", err)

View File

@ -272,7 +272,7 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) {
pubDashService := publicdashboards.NewFakePublicDashboardService(t) pubDashService := publicdashboards.NewFakePublicDashboardService(t)
pubDashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil).Maybe() pubDashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil).Maybe()
middleware := publicdashboards.NewFakePublicDashboardMiddleware(t) 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) guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService)
}) })

View File

@ -238,6 +238,7 @@ type FrontendSettingsDTO struct {
GeomapDisableCustomBaseLayer bool `json:"geomapDisableCustomBaseLayer"` GeomapDisableCustomBaseLayer bool `json:"geomapDisableCustomBaseLayer"`
PublicDashboardAccessToken string `json:"publicDashboardAccessToken"` PublicDashboardAccessToken string `json:"publicDashboardAccessToken"`
PublicDashboardsEnabled bool `json:"publicDashboardsEnabled"`
DateFormats setting.DateFormats `json:"dateFormats,omitempty"` DateFormats setting.DateFormats `json:"dateFormats,omitempty"`

View File

@ -158,6 +158,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
SecureSocksDSProxyEnabled: hs.Cfg.SecureSocksDSProxy.Enabled && hs.Cfg.SecureSocksDSProxy.ShowUI, SecureSocksDSProxyEnabled: hs.Cfg.SecureSocksDSProxy.Enabled && hs.Cfg.SecureSocksDSProxy.ShowUI,
DisableFrontendSandboxForPlugins: hs.Cfg.DisableFrontendSandboxForPlugins, DisableFrontendSandboxForPlugins: hs.Cfg.DisableFrontendSandboxForPlugins,
PublicDashboardAccessToken: c.PublicDashboardAccessToken, PublicDashboardAccessToken: c.PublicDashboardAccessToken,
PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled,
SharedWithMeFolderUID: folder.SharedWithMeFolderUID, SharedWithMeFolderUID: folder.SharedWithMeFolderUID,
BuildInfo: dtos.FrontendSettingsBuildInfoDTO{ BuildInfo: dtos.FrontendSettingsBuildInfoDTO{

View File

@ -48,7 +48,7 @@ var (
}, },
{ {
Name: "publicDashboards", 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, Stage: FeatureStageGeneralAvailability,
Owner: grafanaSharingSquad, Owner: grafanaSharingSquad,
Expression: "true", // enabled by default Expression: "true", // enabled by default

View File

@ -24,7 +24,7 @@ const (
FlagPanelTitleSearch = "panelTitleSearch" FlagPanelTitleSearch = "panelTitleSearch"
// FlagPublicDashboards // 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" FlagPublicDashboards = "publicDashboards"
// FlagPublicDashboardsEmailSharing // FlagPublicDashboardsEmailSharing

View File

@ -358,7 +358,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
Icon: "library-panel", 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{ dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Public dashboards", Text: "Public dashboards",
Id: "dashboards/public", Id: "dashboards/public",

View File

@ -17,16 +17,19 @@ import (
"github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models" . "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/publicdashboards/validation"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
type Api struct { type Api struct {
PublicDashboardService publicdashboards.Service PublicDashboardService publicdashboards.Service
RouteRegister routing.RouteRegister
AccessControl accesscontrol.AccessControl
Features *featuremgmt.FeatureManager
Log log.Logger
Middleware publicdashboards.Middleware Middleware publicdashboards.Middleware
accessControl accesscontrol.AccessControl
cfg *setting.Cfg
features *featuremgmt.FeatureManager
log log.Logger
routeRegister routing.RouteRegister
} }
func ProvideApi( func ProvideApi(
@ -35,21 +38,27 @@ func ProvideApi(
ac accesscontrol.AccessControl, ac accesscontrol.AccessControl,
features *featuremgmt.FeatureManager, features *featuremgmt.FeatureManager,
md publicdashboards.Middleware, md publicdashboards.Middleware,
cfg *setting.Cfg,
) *Api { ) *Api {
api := &Api{ api := &Api{
PublicDashboardService: pd, PublicDashboardService: pd,
RouteRegister: rr,
AccessControl: ac,
Features: features,
Log: log.New("publicdashboards.api"),
Middleware: md, Middleware: md,
accessControl: ac,
cfg: cfg,
features: features,
log: log.New("publicdashboards.api"),
routeRegister: rr,
} }
// attach api if PublicDashboards feature flag is enabled // register endpoints if the feature is enabled
if features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) { if features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && cfg.PublicDashboardsEnabled {
api.RegisterAPIEndpoints() 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 return api
} }
@ -59,35 +68,35 @@ func (api *Api) RegisterAPIEndpoints() {
// Anonymous access to public dashboard route is configured in pkg/api/api.go // 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 // because it is deeply dependent on the HTTPServer.Index() method and would result in a
// circular dependency // 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("/", routing.Wrap(api.ViewPublicDashboard))
apiRoute.Get("/annotations", routing.Wrap(api.GetPublicAnnotations)) apiRoute.Get("/annotations", routing.Wrap(api.GetPublicAnnotations))
apiRoute.Post("/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard)) apiRoute.Post("/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard))
}, api.Middleware.HandleApi) }, api.Middleware.HandleApi)
// Auth endpoints // Auth endpoints
auth := accesscontrol.Middleware(api.AccessControl) auth := accesscontrol.Middleware(api.accessControl)
uidScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(accesscontrol.Parameter(":dashboardUid")) uidScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(accesscontrol.Parameter(":dashboardUid"))
// List public dashboards for org // 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 // 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)), auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, uidScope)),
routing.Wrap(api.GetPublicDashboard)) routing.Wrap(api.GetPublicDashboard))
// Create Public Dashboard // 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)), auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)),
routing.Wrap(api.CreatePublicDashboard)) routing.Wrap(api.CreatePublicDashboard))
// Update Public Dashboard // 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)), auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)),
routing.Wrap(api.UpdatePublicDashboard)) routing.Wrap(api.UpdatePublicDashboard))
// Delete Public dashboard // 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)), auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)),
routing.Wrap(api.DeletePublicDashboard)) routing.Wrap(api.DeletePublicDashboard))
} }

View File

@ -13,7 +13,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/dashboards" "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/org"
"github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models" . "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 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} var anonymousUser = &user.SignedInUser{IsAnonymous: true}
func TestAPIFeatureFlag(t *testing.T) { func TestAPIFeatureDisabled(t *testing.T) {
testCases := []struct { testCases := []struct {
Name string Name string
Method string Method string
@ -71,11 +70,18 @@ func TestAPIFeatureFlag(t *testing.T) {
} }
for _, test := range testCases { 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 := setting.NewCfg()
cfg.PublicDashboardsEnabled = false
service := publicdashboards.NewFakePublicDashboardService(t) service := publicdashboards.NewFakePublicDashboardService(t)
features := featuremgmt.WithFeatures() testServer := setupTestServer(t, cfg, service, userAdmin, true)
testServer := setupTestServer(t, cfg, features, service, nil, userAdmin) 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) response := callAPI(testServer, test.Method, test.Path, nil, t)
assert.Equal(t, http.StatusNotFound, response.Code) 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). service.On("FindAllWithPagination", mock.Anything, mock.Anything, mock.Anything).
Return(test.Response, test.ResponseErr).Maybe() Return(test.Response, test.ResponseErr).Maybe()
cfg := setting.NewCfg() testServer := setupTestServer(t, nil, service, test.User, true)
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
testServer := setupTestServer(t, cfg, features, service, nil, test.User)
response := callAPI(testServer, http.MethodGet, "/api/dashboards/public-dashboards", nil, t) response := callAPI(testServer, http.MethodGet, "/api/dashboards/public-dashboards", nil, t)
assert.Equal(t, test.ExpectedHttpResponse, response.Code) assert.Equal(t, test.ExpectedHttpResponse, response.Code)
@ -259,10 +263,7 @@ func TestAPIDeletePublicDashboard(t *testing.T) {
Return(test.ResponseErr) Return(test.ResponseErr)
} }
cfg := setting.NewCfg() testServer := setupTestServer(t, nil, service, test.User, true)
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
testServer := setupTestServer(t, cfg, features, service, nil, test.User)
response := callAPI(testServer, http.MethodDelete, fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid), nil, t) 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) assert.Equal(t, test.ExpectedHttpResponse, response.Code)
@ -347,16 +348,7 @@ func TestAPIGetPublicDashboard(t *testing.T) {
Return(test.PublicDashboardResult, test.PublicDashboardErr) Return(test.PublicDashboardResult, test.PublicDashboardErr)
} }
cfg := setting.NewCfg() testServer := setupTestServer(t, nil, service, test.User, true)
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
test.User,
)
response := callAPI( response := callAPI(
testServer, testServer,
@ -474,16 +466,7 @@ func TestApiCreatePublicDashboard(t *testing.T) {
Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr) Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr)
} }
cfg := setting.NewCfg() testServer := setupTestServer(t, nil, service, test.User, true)
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
test.User,
)
response := callAPI( response := callAPI(
testServer, testServer,
@ -609,9 +592,6 @@ func TestAPIUpdatePublicDashboard(t *testing.T) {
} }
for _, test := range testCases { for _, test := range testCases {
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
t.Run(test.Name, func(t *testing.T) { t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t) service := publicdashboards.NewFakePublicDashboardService(t)
@ -620,7 +600,7 @@ func TestAPIUpdatePublicDashboard(t *testing.T) {
Return(test.ExpectedResponse, test.ExpectedError) 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) url := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid)
body := strings.NewReader(test.Body) body := strings.NewReader(test.Body)

View File

@ -40,11 +40,12 @@ import (
func setupTestServer( func setupTestServer(
t *testing.T, t *testing.T,
cfg *setting.Cfg, cfg *setting.Cfg,
features *featuremgmt.FeatureManager,
service publicdashboards.Service, service publicdashboards.Service,
db db.DB,
user *user.SignedInUser, user *user.SignedInUser,
ffEnabled bool,
) *web.Mux { ) *web.Mux {
t.Helper()
// build router to register routes // build router to register routes
rr := routing.NewRouteRegister() rr := routing.NewRouteRegister()
@ -56,9 +57,18 @@ func setupTestServer(
// set initial context // set initial context
m.Use(contextProvider(&testContext{user})) m.Use(contextProvider(&testContext{user}))
// build api, this will mount the routes at the same time if features := featuremgmt.WithFeatures()
// featuremgmt.FlagPublicDashboard is enabled if ffEnabled {
ProvideApi(service, rr, ac, features, &Middleware{}) 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 // connect routes to mux
rr.Register(m.Router) rr.Register(m.Router)

View File

@ -72,7 +72,7 @@ func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Respon
return response.Err(err) 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 // swagger:route GET /public/dashboards/{accessToken}/annotations dashboard_public getPublicAnnotations

View File

@ -101,16 +101,7 @@ func TestAPIViewPublicDashboard(t *testing.T) {
service.On("GetPublicDashboardForView", mock.Anything, mock.AnythingOfType("string")). service.On("GetPublicDashboardForView", mock.Anything, mock.AnythingOfType("string")).
Return(test.DashboardResult, test.Err).Maybe() Return(test.DashboardResult, test.Err).Maybe()
cfg := setting.NewCfg() testServer := setupTestServer(t, nil, service, anonymousUser, true)
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
anonymousUser,
)
response := callAPI(testServer, http.MethodGet, response := callAPI(testServer, http.MethodGet,
fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken), 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) { setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) {
service := publicdashboards.NewFakePublicDashboardService(t) service := publicdashboards.NewFakePublicDashboardService(t)
cfg := setting.NewCfg() testServer := setupTestServer(t, nil, service, anonymousUser, true)
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled),
service,
nil,
anonymousUser,
)
return testServer, service return testServer, service
} }
@ -338,6 +320,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
// create public dashboard // create public dashboard
store := publicdashboardsStore.ProvideStore(db, db.Cfg, featuremgmt.WithFeatures()) store := publicdashboardsStore.ProvideStore(db, db.Cfg, featuremgmt.WithFeatures())
cfg := setting.NewCfg() cfg := setting.NewCfg()
cfg.PublicDashboardsEnabled = true
ac := acmock.New() ac := acmock.New()
ws := publicdashboardsService.ProvideServiceWrapper(store) ws := publicdashboardsService.ProvideServiceWrapper(store)
folderStore := folderimpl.ProvideDashboardFolderStore(db) folderStore := folderimpl.ProvideDashboardFolderStore(db)
@ -354,13 +337,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
require.NoError(t, err) require.NoError(t, err)
// setup test server // setup test server
server := setupTestServer(t, server := setupTestServer(t, cfg, pds, anonymousUser, true)
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
pds,
db,
anonymousUser,
)
resp := callAPI(server, http.MethodPost, resp := callAPI(server, http.MethodPost,
fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken), fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken),
@ -436,7 +413,6 @@ func TestAPIGetAnnotations(t *testing.T) {
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) { t.Run(test.Name, func(t *testing.T) {
cfg := setting.NewCfg()
service := publicdashboards.NewFakePublicDashboardService(t) service := publicdashboards.NewFakePublicDashboardService(t)
if test.ExpectedServiceCalled { if test.ExpectedServiceCalled {
@ -444,7 +420,7 @@ func TestAPIGetAnnotations(t *testing.T) {
Return(test.Annotations, test.ServiceError).Once() 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) 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) response := callAPI(testServer, http.MethodGet, path, nil, t)

View File

@ -58,7 +58,7 @@ func TestIntegrationListPublicDashboard(t *testing.T) {
var publicdashboardStore *PublicDashboardStoreImpl var publicdashboardStore *PublicDashboardStoreImpl
setup := func() { 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) quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService)
require.NoError(t, err) require.NoError(t, err)
@ -448,7 +448,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) {
var savedDashboard2 *dashboards.Dashboard var savedDashboard2 *dashboards.Dashboard
setup := func() { 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) quotaService := quotatest.New(false, nil)
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService)
require.NoError(t, err) require.NoError(t, err)
@ -528,7 +528,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
var err error var err error
setup := func() { 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) quotaService := quotatest.New(false, nil)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService)
require.NoError(t, err) require.NoError(t, err)
@ -788,7 +788,7 @@ func TestGetMetrics(t *testing.T) {
var savedDashboard4 *dashboards.Dashboard var savedDashboard4 *dashboards.Dashboard
setup := func() { 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) quotaService := quotatest.New(false, nil)
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService)
require.NoError(t, err) require.NoError(t, err)

View File

@ -541,6 +541,9 @@ type Cfg struct {
// sqlstore package and HTTP middlewares. // sqlstore package and HTTP middlewares.
DatabaseInstrumentQueries bool DatabaseInstrumentQueries bool
// Public dashboards
PublicDashboardsEnabled bool
// Feature Management Settings // Feature Management Settings
FeatureManagement FeatureMgmtSettings 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.UserFacingDefaultError = logSection.Key("user_facing_default_error").MustString("please inspect Grafana server log for details")
cfg.readFeatureManagementConfig() cfg.readFeatureManagementConfig()
cfg.readPublicDashboardsSettings()
return nil return nil
} }
@ -1965,3 +1969,8 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error {
cfg.LiveAllowedOrigins = originPatterns cfg.LiveAllowedOrigins = originPatterns
return nil return nil
} }
func (cfg *Cfg) readPublicDashboardsSettings() {
publicDashboards := cfg.Raw.Section("public_dashboards")
cfg.PublicDashboardsEnabled = publicDashboards.Key("enabled").MustBool(true)
}

View File

@ -6,6 +6,7 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { config, featureEnabled } from '@grafana/runtime'; import { config, featureEnabled } from '@grafana/runtime';
import { useStyles2, TabsBar, Tab } from '@grafana/ui'; import { useStyles2, TabsBar, Tab } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv'; 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 { Page } from '../../core/components/Page/Page';
import { AccessControlAction } from '../../types'; import { AccessControlAction } from '../../types';
@ -46,7 +47,7 @@ export default function UserListPage() {
const hasAccessToAdminUsers = contextSrv.hasPermission(AccessControlAction.UsersRead); const hasAccessToAdminUsers = contextSrv.hasPermission(AccessControlAction.UsersRead);
const hasAccessToOrgUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); const hasAccessToOrgUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead);
const hasEmailSharingEnabled = const hasEmailSharingEnabled =
Boolean(config.featureToggles.publicDashboards) && isPublicDashboardsEnabled() &&
Boolean(config.featureToggles.publicDashboardsEmailSharing) && Boolean(config.featureToggles.publicDashboardsEmailSharing) &&
featureEnabled('publicDashboardsEmailSharing'); featureEnabled('publicDashboardsEmailSharing');

View File

@ -5,6 +5,7 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel, Scene
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
@ -61,7 +62,7 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
} }
} }
if (Boolean(config.featureToggles['publicDashboards'])) { if (isPublicDashboardsEnabled()) {
tabs.push(new SharePublicDashboardTab({ dashboardRef, modalRef: this.getRef() })); tabs.push(new SharePublicDashboardTab({ dashboardRef, modalRef: this.getRef() }));
} }

View File

@ -5,6 +5,7 @@ import { config } from 'app/core/config';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { SharePublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard'; 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 { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard'; import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
@ -56,7 +57,7 @@ function getTabs(panel?: PanelModel, activeTab?: string) {
tabs.push(...customDashboardTabs); tabs.push(...customDashboardTabs);
} }
if (Boolean(config.featureToggles['publicDashboards'])) { if (isPublicDashboardsEnabled()) {
tabs.push({ tabs.push({
label: 'Public dashboard', label: 'Public dashboard',
value: shareDashboardType.publicDashboard, value: shareDashboardType.publicDashboard,

View File

@ -12,6 +12,6 @@ export const NoUpsertPermissionsAlert = ({ mode }: { mode: 'create' | 'edit' })
data-testid={selectors.NoUpsertPermissionsWarningAlert} data-testid={selectors.NoUpsertPermissionsWarningAlert}
bottomSpacing={0} bottomSpacing={0}
> >
Contact your admin to get permission to {mode} create public dashboards Contact your admin to get permission to {mode} public dashboards
</Alert> </Alert>
); );

View File

@ -70,6 +70,7 @@ beforeAll(() => {
beforeEach(() => { beforeEach(() => {
config.featureToggles.publicDashboards = true; config.featureToggles.publicDashboards = true;
config.publicDashboardsEnabled = true;
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
jest.spyOn(contextSrv, 'hasRole').mockReturnValue(true); jest.spyOn(contextSrv, 'hasRole').mockReturnValue(true);
@ -137,7 +138,14 @@ describe('SharePublic', () => {
beforeEach(() => { beforeEach(() => {
server.use(getExistentPublicDashboardResponse()); 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; config.featureToggles.publicDashboards = false;
await renderSharePublicDashboard(undefined, false); await renderSharePublicDashboard(undefined, false);

View File

@ -1,5 +1,5 @@
import { TypedVariableModel } from '@grafana/data'; import { TypedVariableModel } from '@grafana/data';
import { DataSourceWithBackend } from '@grafana/runtime'; import { config, DataSourceWithBackend } from '@grafana/runtime';
import { getConfig } from 'app/core/config'; import { getConfig } from 'app/core/config';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; 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 validEmailRegex = /^[A-Z\d._%+-]+@[A-Z\d.-]+\.[A-Z]{2,}$/i;
export const isPublicDashboardsEnabled = () => {
return Boolean(config.featureToggles.publicDashboards) && config.publicDashboardsEnabled;
};

View File

@ -4,32 +4,33 @@ import { RouteDescriptor } from '../../core/navigation/types';
import { DashboardRoutes } from '../../types'; import { DashboardRoutes } from '../../types';
export const getPublicDashboardRoutes = (): RouteDescriptor[] => { export const getPublicDashboardRoutes = (): RouteDescriptor[] => {
if (config.featureToggles.publicDashboards) { if (!config.publicDashboardsEnabled || !config.featureToggles.publicDashboards) {
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'
)
),
},
];
} }
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'
)
),
},
];
}; };