public dashboards: insert default public dashboard config into database on save (#49131)
This PR adds endpoints for saving and retrieving a public dashboard configuration and and api endpoint to retrieve the public dashboard. All of this is highly experimental and APIs will change. Notably, we will be removing isPublic from the dashboard model and moving it over to the public dashboard table in the next release. Further context can be found here: https://github.com/grafana/grafana/pull/49131#issuecomment-1145456952
This commit is contained in:
+7
-2
@@ -392,8 +392,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
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))
|
||||
dashUidRoute.Get("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetPublicDashboardConfig))
|
||||
dashUidRoute.Post("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.SavePublicDashboardConfig))
|
||||
}
|
||||
|
||||
if hs.ThumbService != nil {
|
||||
@@ -608,6 +608,11 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey))
|
||||
r.Delete("/api/snapshots/:key", reqEditorRole, routing.Wrap(hs.DeleteDashboardSnapshot))
|
||||
|
||||
// Public API
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
|
||||
r.Get("/api/public/dashboards/:uid", routing.Wrap(hs.GetPublicDashboard))
|
||||
}
|
||||
|
||||
// Frontend logs
|
||||
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS)
|
||||
r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
// gets public dashboard
|
||||
func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response {
|
||||
dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err)
|
||||
}
|
||||
return response.JSON(http.StatusOK, dash)
|
||||
}
|
||||
|
||||
// gets public dashboard configuration for dashboard
|
||||
func (hs *HTTPServer) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
|
||||
pdc, err := hs.dashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err)
|
||||
}
|
||||
return response.JSON(http.StatusOK, pdc)
|
||||
}
|
||||
|
||||
// sets public dashboard configuration for dashboard
|
||||
func (hs *HTTPServer) SavePublicDashboardConfig(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,
|
||||
DashboardUid: web.Params(c.Req)[":uid"],
|
||||
PublicDashboardConfig: pdc,
|
||||
}
|
||||
|
||||
pdc, err := hs.dashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto)
|
||||
if err != nil {
|
||||
return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, pdc)
|
||||
}
|
||||
|
||||
// util to help us unpack a dashboard err or use default http code and message
|
||||
func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response {
|
||||
var dashboardErr models.DashboardErr
|
||||
|
||||
if ok := errors.As(err, &dashboardErr); ok {
|
||||
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr)
|
||||
}
|
||||
|
||||
return response.Error(defaultCode, defaultMsg, err)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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)
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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))
|
||||
dashSvc := dashboards.NewFakeDashboardService(t)
|
||||
dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
|
||||
Return(test.publicDashboardConfigResult, test.publicDashboardConfigError)
|
||||
sc.hs.dashboardService = dashSvc
|
||||
|
||||
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))
|
||||
dashSvc := dashboards.NewFakeDashboardService(t)
|
||||
dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")).
|
||||
Return(&models.PublicDashboardConfig{IsPublic: true}, test.saveDashboardError)
|
||||
sc.hs.dashboardService = dashSvc
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
func TestAPIGetPublicDashboard(t *testing.T) {
|
||||
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
|
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures())
|
||||
dashSvc := dashboards.NewFakeDashboardService(t)
|
||||
dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
|
||||
Return(&models.Dashboard{}, nil).Maybe()
|
||||
sc.hs.dashboardService = dashSvc
|
||||
|
||||
setInitCtxSignedInViewer(sc.initCtx)
|
||||
response := callAPI(
|
||||
sc.server,
|
||||
http.MethodGet,
|
||||
"/api/public/dashboards",
|
||||
nil,
|
||||
t,
|
||||
)
|
||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||
response = callAPI(
|
||||
sc.server,
|
||||
http.MethodGet,
|
||||
"/api/public/dashboards/asdf",
|
||||
nil,
|
||||
t,
|
||||
)
|
||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
uid string
|
||||
expectedHttpResponse int
|
||||
publicDashboardResult *models.Dashboard
|
||||
publicDashboardErr error
|
||||
}{
|
||||
{
|
||||
name: "It gets a public dashboard",
|
||||
uid: "pubdash-abcd1234",
|
||||
expectedHttpResponse: http.StatusOK,
|
||||
publicDashboardResult: &models.Dashboard{
|
||||
Uid: "dashboard-abcd1234",
|
||||
},
|
||||
publicDashboardErr: nil,
|
||||
},
|
||||
{
|
||||
name: "It should return 404 if isPublicDashboard is false",
|
||||
uid: "pubdash-abcd1234",
|
||||
expectedHttpResponse: http.StatusNotFound,
|
||||
publicDashboardResult: nil,
|
||||
publicDashboardErr: models.ErrPublicDashboardNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
|
||||
dashSvc := dashboards.NewFakeDashboardService(t)
|
||||
dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
|
||||
Return(test.publicDashboardResult, test.publicDashboardErr)
|
||||
sc.hs.dashboardService = dashSvc
|
||||
|
||||
setInitCtxSignedInViewer(sc.initCtx)
|
||||
response := callAPI(
|
||||
sc.server,
|
||||
http.MethodGet,
|
||||
fmt.Sprintf("/api/public/dashboards/%v", test.uid),
|
||||
nil,
|
||||
t,
|
||||
)
|
||||
|
||||
assert.Equal(t, test.expectedHttpResponse, response.Code)
|
||||
|
||||
if test.publicDashboardErr == nil {
|
||||
var dashResp models.Dashboard
|
||||
err := json.Unmarshal(response.Body.Bytes(), &dashResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.publicDashboardResult.Uid, dashResp.Uid)
|
||||
} else {
|
||||
var errResp struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
err := json.Unmarshal(response.Body.Bytes(), &errResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.publicDashboardErr.Error(), errResp.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIGetPublicDashboardConfig(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))
|
||||
dashSvc := dashboards.NewFakeDashboardService(t)
|
||||
dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
|
||||
Return(test.publicDashboardConfigResult, test.publicDashboardConfigError)
|
||||
sc.hs.dashboardService = dashSvc
|
||||
|
||||
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 response.Code == http.StatusOK {
|
||||
var pdcResp models.PublicDashboardConfig
|
||||
err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.publicDashboardConfigResult, &pdcResp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiSavePublicDashboardConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
dashboardUid string
|
||||
publicDashboardConfig *models.PublicDashboardConfig
|
||||
expectedHttpResponse int
|
||||
saveDashboardError error
|
||||
}{
|
||||
{
|
||||
name: "returns 200 when update persists",
|
||||
dashboardUid: "1",
|
||||
publicDashboardConfig: &models.PublicDashboardConfig{IsPublic: true},
|
||||
expectedHttpResponse: http.StatusOK,
|
||||
saveDashboardError: nil,
|
||||
},
|
||||
{
|
||||
name: "returns 500 when not persisted",
|
||||
expectedHttpResponse: http.StatusInternalServerError,
|
||||
publicDashboardConfig: &models.PublicDashboardConfig{},
|
||||
saveDashboardError: errors.New("backend failed to save"),
|
||||
},
|
||||
{
|
||||
name: "returns 404 when dashboard not found",
|
||||
expectedHttpResponse: http.StatusNotFound,
|
||||
publicDashboardConfig: &models.PublicDashboardConfig{},
|
||||
saveDashboardError: models.ErrDashboardNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
|
||||
|
||||
dashSvc := dashboards.NewFakeDashboardService(t)
|
||||
dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")).
|
||||
Return(&models.PublicDashboardConfig{IsPublic: true}, test.saveDashboardError)
|
||||
sc.hs.dashboardService = dashSvc
|
||||
|
||||
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 {
|
||||
val, err := json.Marshal(test.publicDashboardConfig)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(val), response.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user