add isPublic to dashboard (#48012)
adds toggle to make a dashboard public * config struct for public dashboard config * api endpoints for public dashboard configuration * ui for toggling public dashboard on and off * load public dashboard config on share modal Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
co-authored by
Owen Smallwood
Torkel Ödegaard
parent
156e14e296
commit
c7f8c2cc73
+13
-7
@@ -378,14 +378,20 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
})
|
||||
})
|
||||
|
||||
if hs.ThumbService != nil {
|
||||
dashboardRoute.Get("/uid/:uid/img/:kind/:theme", hs.ThumbService.GetImage)
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
|
||||
dashboardRoute.Post("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.SetImage)
|
||||
dashboardRoute.Put("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.UpdateThumbnailState)
|
||||
dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) {
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
|
||||
dashUidRoute.Get("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetPublicDashboard))
|
||||
dashUidRoute.Post("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.SavePublicDashboard))
|
||||
}
|
||||
}
|
||||
|
||||
if hs.ThumbService != nil {
|
||||
dashUidRoute.Get("/img/:kind/:theme", hs.ThumbService.GetImage)
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
|
||||
dashUidRoute.Post("/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.SetImage)
|
||||
dashUidRoute.Put("/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.UpdateThumbnailState)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
|
||||
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
|
||||
|
||||
+12
-3
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers/filters"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
@@ -332,10 +333,19 @@ func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessContro
|
||||
|
||||
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
|
||||
db := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{})
|
||||
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, db)
|
||||
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, db, featuremgmt.WithFeatures())
|
||||
}
|
||||
|
||||
func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore, store sqlstore.Store) accessControlScenarioContext {
|
||||
func setupHTTPServerWithMockDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, features *featuremgmt.FeatureManager) accessControlScenarioContext {
|
||||
// Use a new conf
|
||||
cfg := setting.NewCfg()
|
||||
db := sqlstore.InitTestDB(t)
|
||||
db.Cfg = setting.NewCfg()
|
||||
|
||||
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, mockstore.NewSQLStoreMock(), features)
|
||||
}
|
||||
|
||||
func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore, store sqlstore.Store, features *featuremgmt.FeatureManager) accessControlScenarioContext {
|
||||
t.Helper()
|
||||
|
||||
if enableAccessControl {
|
||||
@@ -345,7 +355,6 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
|
||||
cfg.RBACEnabled = false
|
||||
db.Cfg.RBACEnabled = false
|
||||
}
|
||||
features := featuremgmt.WithFeatures()
|
||||
|
||||
var acmock *accesscontrolmock.Mock
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
|
||||
Url: dash.GetUrl(),
|
||||
FolderTitle: "General",
|
||||
AnnotationsPermissions: annotationPermissions,
|
||||
IsPublic: dash.IsPublic,
|
||||
}
|
||||
|
||||
// lookup folder title
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// Sets sharing configuration for dashboard
|
||||
func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response {
|
||||
pdc, err := hs.dashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"])
|
||||
|
||||
if errors.Is(err, models.ErrDashboardNotFound) {
|
||||
return response.Error(http.StatusNotFound, "dashboard not found", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "error retrieving public dashboard config", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, pdc)
|
||||
}
|
||||
|
||||
// Sets sharing configuration for dashboard
|
||||
func (hs *HTTPServer) SavePublicDashboard(c *models.ReqContext) response.Response {
|
||||
pdc := &models.PublicDashboardConfig{}
|
||||
|
||||
if err := web.Bind(c.Req, pdc); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
|
||||
dto := dashboards.SavePublicDashboardConfigDTO{
|
||||
OrgId: c.OrgId,
|
||||
Uid: web.Params(c.Req)[":uid"],
|
||||
PublicDashboardConfig: *pdc,
|
||||
}
|
||||
|
||||
pdc, err := hs.dashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto)
|
||||
|
||||
fmt.Println("err:", err)
|
||||
|
||||
if errors.Is(err, models.ErrDashboardNotFound) {
|
||||
return response.Error(http.StatusNotFound, "dashboard not found", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "error updating public dashboard config", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, pdc)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApiRetrieveConfig(t *testing.T) {
|
||||
pdc := &models.PublicDashboardConfig{IsPublic: true}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
dashboardUid string
|
||||
expectedHttpResponse int
|
||||
publicDashboardConfigResult *models.PublicDashboardConfig
|
||||
publicDashboardConfigError error
|
||||
}{
|
||||
{
|
||||
name: "retrieves public dashboard config when dashboard is found",
|
||||
dashboardUid: "1",
|
||||
expectedHttpResponse: http.StatusOK,
|
||||
publicDashboardConfigResult: pdc,
|
||||
publicDashboardConfigError: nil,
|
||||
},
|
||||
{
|
||||
name: "returns 404 when dashboard not found",
|
||||
dashboardUid: "77777",
|
||||
expectedHttpResponse: http.StatusNotFound,
|
||||
publicDashboardConfigResult: nil,
|
||||
publicDashboardConfigError: models.ErrDashboardNotFound,
|
||||
},
|
||||
{
|
||||
name: "returns 500 when internal server error",
|
||||
dashboardUid: "1",
|
||||
expectedHttpResponse: http.StatusInternalServerError,
|
||||
publicDashboardConfigResult: nil,
|
||||
publicDashboardConfigError: errors.New("database broken"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
|
||||
|
||||
sc.hs.dashboardService = &dashboards.FakeDashboardService{
|
||||
PublicDashboardConfigResult: test.publicDashboardConfigResult,
|
||||
PublicDashboardConfigError: test.publicDashboardConfigError,
|
||||
}
|
||||
|
||||
setInitCtxSignedInViewer(sc.initCtx)
|
||||
response := callAPI(
|
||||
sc.server,
|
||||
http.MethodGet,
|
||||
"/api/dashboards/uid/1/public-config",
|
||||
nil,
|
||||
t,
|
||||
)
|
||||
|
||||
assert.Equal(t, test.expectedHttpResponse, response.Code)
|
||||
|
||||
if test.expectedHttpResponse == http.StatusOK {
|
||||
var pdcResp models.PublicDashboardConfig
|
||||
err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.publicDashboardConfigResult, &pdcResp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiPersistsValue(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
dashboardUid string
|
||||
expectedHttpResponse int
|
||||
saveDashboardError error
|
||||
}{
|
||||
{
|
||||
name: "returns 200 when update persists",
|
||||
dashboardUid: "1",
|
||||
expectedHttpResponse: http.StatusOK,
|
||||
saveDashboardError: nil,
|
||||
},
|
||||
{
|
||||
name: "returns 500 when not persisted",
|
||||
expectedHttpResponse: http.StatusInternalServerError,
|
||||
saveDashboardError: errors.New("backend failed to save"),
|
||||
},
|
||||
{
|
||||
name: "returns 404 when dashboard not found",
|
||||
expectedHttpResponse: http.StatusNotFound,
|
||||
saveDashboardError: models.ErrDashboardNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
|
||||
|
||||
sc.hs.dashboardService = &dashboards.FakeDashboardService{
|
||||
PublicDashboardConfigResult: &models.PublicDashboardConfig{IsPublic: true},
|
||||
PublicDashboardConfigError: test.saveDashboardError,
|
||||
}
|
||||
|
||||
setInitCtxSignedInViewer(sc.initCtx)
|
||||
response := callAPI(
|
||||
sc.server,
|
||||
http.MethodPost,
|
||||
"/api/dashboards/uid/1/public-config",
|
||||
strings.NewReader(`{ "isPublic": true }`),
|
||||
t,
|
||||
)
|
||||
|
||||
assert.Equal(t, test.expectedHttpResponse, response.Code)
|
||||
|
||||
// check the result if it's a 200
|
||||
if response.Code == http.StatusOK {
|
||||
respJSON, _ := simplejson.NewJson(response.Body.Bytes())
|
||||
val, _ := respJSON.Get("isPublic").Bool()
|
||||
assert.Equal(t, true, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ type DashboardMeta struct {
|
||||
Provisioned bool `json:"provisioned"`
|
||||
ProvisionedExternalId string `json:"provisionedExternalId"`
|
||||
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
}
|
||||
type AnnotationPermission struct {
|
||||
Dashboard AnnotationActions `json:"dashboard"`
|
||||
|
||||
Reference in New Issue
Block a user