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:
Jeff Levin 2022-06-02 18:27:23 -08:00 committed by GitHub
parent efca93a3f3
commit 52ed651958
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1103 additions and 318 deletions

View File

@ -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),

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -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())
}
})
}
}

View File

@ -206,10 +206,6 @@ type Dashboard struct {
Data *simplejson.Json
}
type PublicDashboardConfig struct {
IsPublic bool `json:"isPublic"`
}
func (d *Dashboard) SetId(id int64) {
d.Id = id
d.Data.Set("id", id)
@ -417,12 +413,6 @@ type DeleteOrphanedProvisionedDashboardsCommand struct {
ReaderNames []string
}
type SavePublicDashboardConfigCommand struct {
Uid string
OrgId int64
PublicDashboardConfig PublicDashboardConfig
}
//
// QUERIES
//

View File

@ -0,0 +1,43 @@
package models
var (
ErrPublicDashboardFailedGenerateUniqueUid = DashboardErr{
Reason: "Failed to generate unique dashboard id",
StatusCode: 500,
}
ErrPublicDashboardNotFound = DashboardErr{
Reason: "Public dashboard not found",
StatusCode: 404,
Status: "not-found",
}
ErrPublicDashboardIdentifierNotSet = DashboardErr{
Reason: "No Uid for public dashboard specified",
StatusCode: 400,
}
)
type PublicDashboardConfig struct {
IsPublic bool `json:"isPublic"`
PublicDashboard PublicDashboard `json:"publicDashboard"`
}
type PublicDashboard struct {
Uid string `json:"uid" xorm:"uid"`
DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"`
OrgId int64 `json:"orgId" xorm:"org_id"`
TimeSettings string `json:"timeSettings" xorm:"time_settings"`
}
func (pd PublicDashboard) TableName() string {
return "dashboard_public_config"
}
//
// COMMANDS
//
type SavePublicDashboardConfigCommand struct {
DashboardUid string
OrgId int64
PublicDashboardConfig PublicDashboardConfig
}

View File

@ -17,6 +17,7 @@ type DashboardService interface {
GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error
GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error
GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error
GetPublicDashboard(ctx context.Context, publicDashboardUid string) (*models.Dashboard, error)
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error)
HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error
HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error
@ -63,6 +64,7 @@ type Store interface {
GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error)
GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error)
GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error)
HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error
HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error
// SaveAlerts saves dashboard alerts.

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.12.2. DO NOT EDIT.
// Code generated by mockery v2.12.1. DO NOT EDIT.
package dashboards
@ -146,6 +146,29 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *models
return r0
}
// GetPublicDashboard provides a mock function with given fields: ctx, publicDashboardUid
func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, publicDashboardUid string) (*models.Dashboard, error) {
ret := _m.Called(ctx, publicDashboardUid)
var r0 *models.Dashboard
if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok {
r0 = rf(ctx, publicDashboardUid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Dashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, publicDashboardUid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid
func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
ret := _m.Called(ctx, orgId, dashboardUid)

View File

@ -192,47 +192,6 @@ func (d *DashboardStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models
return cmd.Result, err
}
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
var result []*models.Dashboard
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
return sess.Where("org_id = ? AND uid= ?", orgId, dashboardUid).Find(&result)
})
if len(result) == 0 {
return nil, models.ErrDashboardNotFound
}
pdc := &models.PublicDashboardConfig{
IsPublic: result[0].IsPublic,
}
return pdc, err
}
// stores public dashboard configuration
func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) {
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
affectedRowCount, err := sess.Table("dashboard").Where("org_id = ? AND uid = ?", cmd.OrgId, cmd.Uid).Update(map[string]interface{}{"is_public": cmd.PublicDashboardConfig.IsPublic})
if err != nil {
return err
}
if affectedRowCount == 0 {
return models.ErrDashboardNotFound
}
return nil
})
if err != nil {
return nil, err
}
return &cmd.PublicDashboardConfig, nil
}
func (d *DashboardStore) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error {
return d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// delete existing items

View File

@ -0,0 +1,157 @@
package database
import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
)
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) {
if uid == "" {
return nil, nil, models.ErrPublicDashboardIdentifierNotSet
}
// get public dashboard
pdRes := &models.PublicDashboard{Uid: uid}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
has, err := sess.Get(pdRes)
if err != nil {
return err
}
if !has {
return models.ErrPublicDashboardNotFound
}
return nil
})
if err != nil {
return nil, nil, err
}
// find dashboard
dashRes := &models.Dashboard{OrgId: pdRes.OrgId, Uid: pdRes.DashboardUid}
err = d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
has, err := sess.Get(dashRes)
if err != nil {
return err
}
if !has {
return models.ErrPublicDashboardNotFound
}
return nil
})
if err != nil {
return nil, nil, err
}
return pdRes, dashRes, err
}
// generates a new unique uid to retrieve a public dashboard
func generateNewPublicDashboardUid(sess *sqlstore.DBSession) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
exists, err := sess.Get(&models.PublicDashboard{Uid: uid})
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", models.ErrPublicDashboardFailedGenerateUniqueUid
}
// retrieves public dashboard configuration
func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
if dashboardUid == "" {
return nil, models.ErrDashboardIdentifierNotSet
}
// get dashboard and publicDashboard
dashRes := &models.Dashboard{OrgId: orgId, Uid: dashboardUid}
pdRes := &models.PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
// dashboard
has, err := sess.Get(dashRes)
if err != nil {
return err
}
if !has {
return models.ErrDashboardNotFound
}
// publicDashboard
_, err = sess.Get(pdRes)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
pdc := &models.PublicDashboardConfig{
IsPublic: dashRes.IsPublic,
PublicDashboard: *pdRes,
}
return pdc, err
}
// persists public dashboard configuration
func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) {
if len(cmd.PublicDashboardConfig.PublicDashboard.DashboardUid) == 0 {
return nil, models.ErrDashboardIdentifierNotSet
}
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
// update isPublic on dashboard entry
affectedRowCount, err := sess.Table("dashboard").Where("org_id = ? AND uid = ?", cmd.OrgId, cmd.DashboardUid).Update(map[string]interface{}{"is_public": cmd.PublicDashboardConfig.IsPublic})
if err != nil {
return err
}
if affectedRowCount == 0 {
return models.ErrDashboardNotFound
}
// update dashboard_public_config
// if we have a uid, public dashboard config exists. delete it otherwise generate a uid
if cmd.PublicDashboardConfig.PublicDashboard.Uid != "" {
if _, err = sess.Exec("DELETE FROM dashboard_public_config WHERE uid=?", cmd.PublicDashboardConfig.PublicDashboard.Uid); err != nil {
return err
}
} else {
uid, err := generateNewPublicDashboardUid(sess)
if err != nil {
return errutil.Wrapf(err, "Failed to generate UID for public dashboard")
}
cmd.PublicDashboardConfig.PublicDashboard.Uid = uid
}
_, err = sess.Insert(&cmd.PublicDashboardConfig.PublicDashboard)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return &cmd.PublicDashboardConfig, nil
}

View File

@ -0,0 +1,233 @@
//go:build integration
// +build integration
package database
import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// GetPublicDashboard
func TestGetPublicDashboard(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var savedDashboard *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t)
dashboardStore = ProvideDashboardStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("returns PublicDashboard and Dashboard", func(t *testing.T) {
setup()
pdc, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "abc1234",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
},
})
require.NoError(t, err)
pd, d, err := dashboardStore.GetPublicDashboard("abc1234")
require.NoError(t, err)
assert.Equal(t, pd, &pdc.PublicDashboard)
assert.Equal(t, d.Uid, pdc.PublicDashboard.DashboardUid)
})
t.Run("returns ErrPublicDashboardNotFound with empty uid", func(t *testing.T) {
setup()
_, _, err := dashboardStore.GetPublicDashboard("")
require.Error(t, models.ErrPublicDashboardIdentifierNotSet, err)
})
t.Run("returns ErrPublicDashboardNotFound when PublicDashboard not found", func(t *testing.T) {
setup()
_, _, err := dashboardStore.GetPublicDashboard("zzzzzz")
require.Error(t, models.ErrPublicDashboardNotFound, err)
})
t.Run("returns ErrDashboardNotFound when Dashboard not found", func(t *testing.T) {
setup()
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "abc1234",
DashboardUid: "nevergonnafindme",
OrgId: savedDashboard.OrgId,
},
},
})
require.NoError(t, err)
_, _, err = dashboardStore.GetPublicDashboard("abc1234")
require.Error(t, models.ErrDashboardNotFound, err)
})
}
// GetPublicDashboardConfig
func TestGetPublicDashboardConfig(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var savedDashboard *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t)
dashboardStore = ProvideDashboardStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
t.Run("returns isPublic and set dashboardUid and orgId", func(t *testing.T) {
setup()
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, &models.PublicDashboardConfig{IsPublic: false, PublicDashboard: models.PublicDashboard{DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}}, pdc)
})
t.Run("returns dashboard errDashboardIdentifierNotSet", func(t *testing.T) {
setup()
_, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, "")
require.Error(t, models.ErrDashboardIdentifierNotSet, err)
})
t.Run("returns isPublic along with public dashboard when exists", func(t *testing.T) {
setup()
// insert test public dashboard
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: "{from: now, to: then}",
},
},
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, resp, pdc)
})
}
// SavePublicDashboardConfig
func TestSavePublicDashboardConfig(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var dashboardStore *DashboardStore
var savedDashboard *models.Dashboard
var savedDashboard2 *models.Dashboard
setup := func() {
sqlStore = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore = ProvideDashboardStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true)
}
t.Run("saves new public dashboard", func(t *testing.T) {
setup()
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: "pubdash-uid",
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
},
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
//verify saved response and queried response are the same
assert.Equal(t, resp, pdc)
// verify we have a valid uid
assert.True(t, util.IsValidShortUID(pdc.PublicDashboard.Uid))
// verify we didn't update all dashboards
pdc2, err := dashboardStore.GetPublicDashboardConfig(savedDashboard2.OrgId, savedDashboard2.Uid)
assert.False(t, pdc2.IsPublic)
})
t.Run("returns ErrDashboardIdentifierNotSet", func(t *testing.T) {
setup()
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
DashboardUid: "",
OrgId: savedDashboard.OrgId,
},
},
})
require.Error(t, models.ErrDashboardIdentifierNotSet, err)
})
t.Run("overwrites existing public dashboard", func(t *testing.T) {
setup()
pdUid := util.GenerateShortUID()
// insert initial record
_, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
},
},
})
require.NoError(t, err)
// update initial record
resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
PublicDashboardConfig: models.PublicDashboardConfig{
IsPublic: false,
PublicDashboard: models.PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
OrgId: savedDashboard.OrgId,
TimeSettings: "{}",
},
},
})
require.NoError(t, err)
pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
assert.Equal(t, resp, pdc)
})
}

View File

@ -16,9 +16,9 @@ type SaveDashboardDTO struct {
}
type SavePublicDashboardConfigDTO struct {
Uid string
DashboardUid string
OrgId int64
PublicDashboardConfig models.PublicDashboardConfig
PublicDashboardConfig *models.PublicDashboardConfig
}
type DashboardSearchProjection struct {

View File

@ -0,0 +1,60 @@
package service
import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
)
// Gets public dashboard via generated Uid
func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
pdc, d, err := dr.dashboardStore.GetPublicDashboard(dashboardUid)
if err != nil {
return nil, err
}
if pdc == nil || d == nil {
return nil, models.ErrPublicDashboardNotFound
}
if !d.IsPublic {
return nil, models.ErrPublicDashboardNotFound
}
// FIXME insert logic to substitute pdc.TimeSettings into d
return d, nil
}
// GetPublicDashboardConfig is a helper method to retrieve the public dashboard configuration for a given dashboard from the database
func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
pdc, err := dr.dashboardStore.GetPublicDashboardConfig(orgId, dashboardUid)
if err != nil {
return nil, err
}
return pdc, nil
}
// SavePublicDashboardConfig is a helper method to persist the sharing config
// to the database. It handles validations for sharing config and persistence
func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) {
cmd := models.SavePublicDashboardConfigCommand{
DashboardUid: dto.DashboardUid,
OrgId: dto.OrgId,
PublicDashboardConfig: *dto.PublicDashboardConfig,
}
// Eventually we want this to propagate to array of public dashboards
cmd.PublicDashboardConfig.PublicDashboard.OrgId = dto.OrgId
cmd.PublicDashboardConfig.PublicDashboard.DashboardUid = dto.DashboardUid
pdc, err := dr.dashboardStore.SavePublicDashboardConfig(cmd)
if err != nil {
return nil, err
}
return pdc, nil
}

View File

@ -0,0 +1,162 @@
package service
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestGetPublicDashboard(t *testing.T) {
type storeResp struct {
pd *models.PublicDashboard
d *models.Dashboard
err error
}
testCases := []struct {
name string
uid string
storeResp *storeResp
errResp error
dashResp *models.Dashboard
}{
{
name: "returns a dashboard",
uid: "abc123",
storeResp: &storeResp{pd: &models.PublicDashboard{}, d: &models.Dashboard{IsPublic: true}, err: nil},
errResp: nil,
dashResp: &models.Dashboard{IsPublic: true},
},
{
name: "returns ErrPublicDashboardNotFound when isPublic is false",
uid: "abc123",
storeResp: &storeResp{pd: &models.PublicDashboard{}, d: &models.Dashboard{IsPublic: false}, err: nil},
errResp: models.ErrPublicDashboardNotFound,
dashResp: nil,
},
{
name: "returns ErrPublicDashboardNotFound if PublicDashboard missing",
uid: "abc123",
storeResp: &storeResp{pd: nil, d: nil, err: nil},
errResp: models.ErrPublicDashboardNotFound,
dashResp: nil,
},
{
name: "returns ErrPublicDashboardNotFound if Dashboard missing",
uid: "abc123",
storeResp: &storeResp{pd: nil, d: nil, err: nil},
errResp: models.ErrPublicDashboardNotFound,
dashResp: nil,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
fakeStore := dashboards.FakeDashboardStore{}
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: &fakeStore,
}
fakeStore.On("GetPublicDashboard", mock.Anything).
Return(test.storeResp.pd, test.storeResp.d, test.storeResp.err)
dashboard, err := service.GetPublicDashboard(context.Background(), test.uid)
if test.errResp != nil {
assert.Error(t, test.errResp, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, test.dashResp, dashboard)
})
}
}
func TestSavePublicDashboard(t *testing.T) {
t.Run("gets PublicDashboard.orgId and PublicDashboard.DashboardUid set from SavePublicDashboardConfigDTO", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
service := &DashboardServiceImpl{
log: log.New("test.logger"),
dashboardStore: dashboardStore,
}
dto := &dashboards.SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
PublicDashboardConfig: &models.PublicDashboardConfig{
IsPublic: true,
PublicDashboard: models.PublicDashboard{
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
},
},
}
pdc, err := service.SavePublicDashboardConfig(context.Background(), dto)
require.NoError(t, err)
assert.Equal(t, dashboard.Uid, pdc.PublicDashboard.DashboardUid)
assert.Equal(t, dashboard.OrgId, pdc.PublicDashboard.OrgId)
})
t.Run("PLACEHOLDER - dashboard with template variables cannot be saved", func(t *testing.T) {
//sqlStore := sqlstore.InitTestDB(t)
//dashboardStore := database.ProvideDashboardStore(sqlStore)
//dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
//service := &DashboardServiceImpl{
//log: log.New("test.logger"),
//dashboardStore: dashboardStore,
//}
//dto := &dashboards.SavePublicDashboardConfigDTO{
//DashboardUid: dashboard.Uid,
//OrgId: dashboard.OrgId,
//PublicDashboardConfig: &models.PublicDashboardConfig{
//IsPublic: true,
//PublicDashboard: models.PublicDashboard{
//DashboardUid: "NOTTHESAME",
//OrgId: 9999999,
//},
//},
//}
//pdc, err := service.SavePublicDashboardConfig(context.Background(), dto)
//require.NoError(t, err)
//assert.Equal(t, dashboard.Uid, pdc.PublicDashboard.DashboardUid)
//assert.Equal(t, dashboard.OrgId, pdc.PublicDashboard.OrgId)
})
}
func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, title string, orgId int64,
folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
t.Helper()
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: folderId,
IsFolder: isFolder,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
dash, err := dashboardStore.SaveDashboard(cmd)
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.Id)
dash.Data.Set("uid", dash.Uid)
return dash
}

View File

@ -344,33 +344,6 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar
return dash, nil
}
// GetPublicDashboardConfig is a helper method to retrieve the public dashboard configuration for a given dashboard from the database
func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
pdc, err := dr.dashboardStore.GetPublicDashboardConfig(orgId, dashboardUid)
if err != nil {
return nil, err
}
return pdc, nil
}
// SavePublicDashboardConfig is a helper method to persist the sharing config
// to the database. It handles validations for sharing config and persistence
func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) {
cmd := models.SavePublicDashboardConfigCommand{
Uid: dto.Uid,
OrgId: dto.OrgId,
PublicDashboardConfig: dto.PublicDashboardConfig,
}
pdc, err := dr.dashboardStore.SavePublicDashboardConfig(cmd)
if err != nil {
return nil, err
}
return pdc, nil
}
// DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for
// operations by the user where we want to make sure user does not delete provisioned dashboard.
func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error {

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.12.2. DO NOT EDIT.
// Code generated by mockery v2.12.1. DO NOT EDIT.
package dashboards
@ -289,6 +289,38 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dash
return r0, r1
}
// GetPublicDashboard provides a mock function with given fields: uid
func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) {
ret := _m.Called(uid)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(string) *models.PublicDashboard); ok {
r0 = rf(uid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 *models.Dashboard
if rf, ok := ret.Get(1).(func(string) *models.Dashboard); ok {
r1 = rf(uid)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*models.Dashboard)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(string) error); ok {
r2 = rf(uid)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetPublicDashboardConfig provides a mock function with given fields: orgId, dashboardUid
func (_m *FakeDashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
ret := _m.Called(orgId, dashboardUid)

View File

@ -8,19 +8,26 @@ func addPublicDashboardMigration(mg *Migrator) {
var dashboardPublicCfgV1 = Table{
Name: "dashboard_public_config",
Columns: []*Column{
{Name: "uid", Type: DB_BigInt, IsPrimaryKey: true},
{Name: "uid", Type: DB_NVarchar, Length: 40, IsPrimaryKey: true},
{Name: "dashboard_uid", Type: DB_NVarchar, Length: 40, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "time_settings", Type: DB_Text, Nullable: false},
{Name: "refresh_rate", Type: DB_Int, Nullable: false, Default: "30"},
{Name: "template_variables", Type: DB_MediumText, Nullable: true},
{Name: "time_variables", Type: DB_Text, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"uid"}, Type: UniqueIndex},
{Cols: []string{"org_id", "dashboard_uid"}},
},
}
mg.AddMigration("create dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1))
// table has no dependencies and was created with incorrect pkey type.
// drop then recreate with correct values
addDropAllIndicesMigrations(mg, "v1", dashboardPublicCfgV1)
mg.AddMigration("Drop old dashboard public config table", NewDropTableMigration("dashboard_public_config"))
// recreate table with proper primary key type
mg.AddMigration("recreate dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1))
addTableIndicesMigrations(mg, "v1", dashboardPublicCfgV1)
}

View File

@ -1,10 +1,27 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { BackendSrv } from '@grafana/runtime';
import config from 'app/core/config';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { ShareModal } from './ShareModal';
import { PublicDashboardConfig } from './SharePublicDashboardUtils';
// Mock api request
const publicDashboardconfigResp: PublicDashboardConfig = {
isPublic: true,
publicDashboard: { uid: '', dashboardUid: '' },
};
const backendSrv = {
get: () => publicDashboardconfigResp,
} as unknown as BackendSrv;
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
getBackendSrv: () => backendSrv,
}));
jest.mock('app/core/core', () => {
return {

View File

@ -1,12 +1,13 @@
import React, { useState, useEffect } from 'react';
import { Button, Field, Switch } from '@grafana/ui';
import { Button, Field, Switch, Alert } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { VariableModel } from 'app/features/variables/types';
import { dispatch } from 'app/store/store';
import {
dashboardCanBePublic,
dashboardHasTemplateVariables,
getPublicDashboardConfig,
savePublicDashboardConfig,
PublicDashboardConfig,
@ -15,46 +16,45 @@ import { ShareModalTabProps } from './types';
interface Props extends ShareModalTabProps {}
// 1. write test for dashboardCanBePublic
// 2. figure out how to disable the switch
export const SharePublicDashboard = (props: Props) => {
const [publicDashboardConfig, setPublicDashboardConfig] = useState<PublicDashboardConfig>({ isPublic: false });
const dashboardUid = props.dashboard.uid;
const [publicDashboardConfig, setPublicDashboardConfig] = useState<PublicDashboardConfig>({
isPublic: false,
publicDashboard: { uid: '', dashboardUid },
});
const [dashboardVariables, setDashboardVariables] = useState<VariableModel[]>([]);
useEffect(() => {
getPublicDashboardConfig(dashboardUid)
.then((pdc: PublicDashboardConfig) => {
setPublicDashboardConfig(pdc);
})
.catch(() => {
dispatch(notifyApp(createErrorNotification('Failed to retrieve public dashboard config')));
});
}, [dashboardUid]);
setDashboardVariables(props.dashboard.getVariables());
getPublicDashboardConfig(dashboardUid, setPublicDashboardConfig).catch();
}, [props, dashboardUid]);
const onSavePublicConfig = () => {
// verify dashboard can be public
if (!dashboardCanBePublic(props.dashboard)) {
dispatch(notifyApp(createErrorNotification('This dashboard cannot be made public')));
if (dashboardHasTemplateVariables(dashboardVariables)) {
dispatch(
notifyApp(createErrorNotification('This dashboard cannot be made public because it has template variables'))
);
return;
}
try {
savePublicDashboardConfig(props.dashboard.uid, publicDashboardConfig);
dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
} catch (err) {
console.error('Error while making dashboard public', err);
dispatch(notifyApp(createErrorNotification('Error making dashboard public')));
}
savePublicDashboardConfig(props.dashboard.uid, publicDashboardConfig, setPublicDashboardConfig).catch();
};
return (
<>
{dashboardHasTemplateVariables(dashboardVariables) && (
<Alert severity="warning" title="dashboard cannot be public">
This dashboard cannot be made public because it has template variables
</Alert>
)}
<p className="share-modal-info-text">Public Dashboard Configuration</p>
<Field label="Enabled" description="Configures whether current dashboard can be available publicly">
<Switch
id="share-current-time-range"
disabled={!dashboardCanBePublic(props.dashboard)}
disabled={dashboardHasTemplateVariables(dashboardVariables)}
value={publicDashboardConfig?.isPublic}
onChange={() =>
setPublicDashboardConfig((state) => {

View File

@ -1,17 +1,16 @@
import { DashboardModel } from 'app/features/dashboard/state';
import { VariableModel } from 'app/features/variables/types';
import { dashboardCanBePublic } from './SharePublicDashboardUtils';
import { dashboardHasTemplateVariables } from './SharePublicDashboardUtils';
describe('dashboardCanBePublic', () => {
it('can be public with no template variables', () => {
//@ts-ignore
const dashboard: DashboardModel = { templating: { list: [] } };
expect(dashboardCanBePublic(dashboard)).toBe(true);
describe('dashboardHasTemplateVariables', () => {
it('false', () => {
let variables: VariableModel[] = [];
expect(dashboardHasTemplateVariables(variables)).toBe(false);
});
it('cannot be public with template variables', () => {
it('true', () => {
//@ts-ignore
const dashboard: DashboardModel = { templating: { list: [{}] } };
expect(dashboardCanBePublic(dashboard)).toBe(false);
let variables: VariableModel[] = ['a'];
expect(dashboardHasTemplateVariables(variables)).toBe(true);
});
});

View File

@ -1,21 +1,43 @@
import { getBackendSrv } from '@grafana/runtime';
import { DashboardModel } from 'app/features/dashboard/state';
import { notifyApp } from 'app/core/actions';
import { createSuccessNotification } from 'app/core/copy/appNotification';
import { VariableModel } from 'app/features/variables/types';
import { dispatch } from 'app/store/store';
import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard';
export interface PublicDashboardConfig {
isPublic: boolean;
publicDashboard: {
uid: string;
dashboardUid: string;
timeSettings?: object;
};
}
export interface DashboardResponse {
dashboard: DashboardDataDTO;
meta: DashboardMeta;
}
export const dashboardCanBePublic = (dashboard: DashboardModel): boolean => {
return dashboard?.templating?.list.length === 0;
export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => {
return variables.length > 0;
};
export const getPublicDashboardConfig = async (dashboardUid: string) => {
export const getPublicDashboardConfig = async (
dashboardUid: string,
setPublicDashboardConfig: React.Dispatch<React.SetStateAction<PublicDashboardConfig>>
) => {
const url = `/api/dashboards/uid/${dashboardUid}/public-config`;
return getBackendSrv().get(url);
const pdResp: PublicDashboardConfig = await getBackendSrv().get(url);
setPublicDashboardConfig(pdResp);
};
export const savePublicDashboardConfig = async (dashboardUid: string, conf: PublicDashboardConfig) => {
const payload = { isPublic: conf.isPublic };
export const savePublicDashboardConfig = async (
dashboardUid: string,
publicDashboardConfig: PublicDashboardConfig,
setPublicDashboardConfig: Function
) => {
const url = `/api/dashboards/uid/${dashboardUid}/public-config`;
return getBackendSrv().post(url, payload);
const pdResp: PublicDashboardConfig = await getBackendSrv().post(url, publicDashboardConfig);
dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
setPublicDashboardConfig(pdResp);
};