mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 07:52:28 +08:00
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:
@ -378,14 +378,20 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if hs.ThumbService != nil {
|
dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) {
|
||||||
dashboardRoute.Get("/uid/:uid/img/:kind/:theme", hs.ThumbService.GetImage)
|
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) {
|
if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
|
||||||
dashboardRoute.Post("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.SetImage)
|
dashUidRoute.Post("/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.SetImage)
|
||||||
dashboardRoute.Put("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.UpdateThumbnailState)
|
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("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
|
||||||
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
|
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
|
||||||
|
@ -40,6 +40,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||||
"github.com/grafana/grafana/pkg/services/searchusers/filters"
|
"github.com/grafana/grafana/pkg/services/searchusers/filters"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"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/setting"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"github.com/grafana/grafana/pkg/web/webtest"
|
"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 {
|
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
|
||||||
db := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{})
|
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()
|
t.Helper()
|
||||||
|
|
||||||
if enableAccessControl {
|
if enableAccessControl {
|
||||||
@ -345,7 +355,6 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
|
|||||||
cfg.RBACEnabled = false
|
cfg.RBACEnabled = false
|
||||||
db.Cfg.RBACEnabled = false
|
db.Cfg.RBACEnabled = false
|
||||||
}
|
}
|
||||||
features := featuremgmt.WithFeatures()
|
|
||||||
|
|
||||||
var acmock *accesscontrolmock.Mock
|
var acmock *accesscontrolmock.Mock
|
||||||
|
|
||||||
|
@ -146,6 +146,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
|
|||||||
Url: dash.GetUrl(),
|
Url: dash.GetUrl(),
|
||||||
FolderTitle: "General",
|
FolderTitle: "General",
|
||||||
AnnotationsPermissions: annotationPermissions,
|
AnnotationsPermissions: annotationPermissions,
|
||||||
|
IsPublic: dash.IsPublic,
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup folder title
|
// lookup folder title
|
||||||
|
56
pkg/api/dashboard_public_config.go
Normal file
56
pkg/api/dashboard_public_config.go
Normal file
@ -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)
|
||||||
|
}
|
134
pkg/api/dashboard_public_config_test.go
Normal file
134
pkg/api/dashboard_public_config_test.go
Normal file
@ -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"`
|
Provisioned bool `json:"provisioned"`
|
||||||
ProvisionedExternalId string `json:"provisionedExternalId"`
|
ProvisionedExternalId string `json:"provisionedExternalId"`
|
||||||
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
|
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
|
||||||
|
IsPublic bool `json:"isPublic"`
|
||||||
}
|
}
|
||||||
type AnnotationPermission struct {
|
type AnnotationPermission struct {
|
||||||
Dashboard AnnotationActions `json:"dashboard"`
|
Dashboard AnnotationActions `json:"dashboard"`
|
||||||
|
@ -199,11 +199,16 @@ type Dashboard struct {
|
|||||||
FolderId int64
|
FolderId int64
|
||||||
IsFolder bool
|
IsFolder bool
|
||||||
HasAcl bool
|
HasAcl bool
|
||||||
|
IsPublic bool
|
||||||
|
|
||||||
Title string
|
Title string
|
||||||
Data *simplejson.Json
|
Data *simplejson.Json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PublicDashboardConfig struct {
|
||||||
|
IsPublic bool `json:"isPublic"`
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Dashboard) SetId(id int64) {
|
func (d *Dashboard) SetId(id int64) {
|
||||||
d.Id = id
|
d.Id = id
|
||||||
d.Data.Set("id", id)
|
d.Data.Set("id", id)
|
||||||
@ -411,6 +416,12 @@ type DeleteOrphanedProvisionedDashboardsCommand struct {
|
|||||||
ReaderNames []string
|
ReaderNames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SavePublicDashboardConfigCommand struct {
|
||||||
|
Uid string
|
||||||
|
OrgId int64
|
||||||
|
PublicDashboardConfig PublicDashboardConfig
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// QUERIES
|
// QUERIES
|
||||||
//
|
//
|
||||||
|
@ -6,9 +6,18 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Generating mocks is handled by vektra/mockery
|
||||||
|
// 1. install go mockery https://github.com/vektra/mockery#go-install
|
||||||
|
// 2. add your method to the relevant services
|
||||||
|
// 3. from the same directory as this file run `go generate` and it will update the mock
|
||||||
|
// If you don't see any output, this most likely means your OS can't find the mockery binary
|
||||||
|
// `which mockery` to confirm and follow one of the installation methods
|
||||||
|
|
||||||
// DashboardService is a service for operating on dashboards.
|
// DashboardService is a service for operating on dashboards.
|
||||||
type DashboardService interface {
|
type DashboardService interface {
|
||||||
SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error)
|
SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error)
|
||||||
|
GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error)
|
||||||
|
SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error)
|
||||||
ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error)
|
ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||||
DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error
|
DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error
|
||||||
MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error
|
MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error
|
||||||
@ -45,6 +54,8 @@ type Store interface {
|
|||||||
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
||||||
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
|
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
|
||||||
SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error)
|
SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error)
|
||||||
|
SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error)
|
||||||
|
GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error)
|
||||||
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
|
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
|
||||||
DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error
|
DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error
|
||||||
// SaveAlerts saves dashboard alerts.
|
// SaveAlerts saves dashboard alerts.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
// Code generated by mockery v2.12.1. DO NOT EDIT.
|
||||||
|
|
||||||
package dashboards
|
package dashboards
|
||||||
|
|
||||||
@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
models "github.com/grafana/grafana/pkg/models"
|
models "github.com/grafana/grafana/pkg/models"
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
testing "testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FakeDashboardProvisioning is an autogenerated mock type for the DashboardProvisioningService type
|
// FakeDashboardProvisioning is an autogenerated mock type for the DashboardProvisioningService type
|
||||||
@ -170,3 +172,13 @@ func (_m *FakeDashboardProvisioning) UnprovisionDashboard(ctx context.Context, d
|
|||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFakeDashboardProvisioning creates a new instance of FakeDashboardProvisioning. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewFakeDashboardProvisioning(t testing.TB) *FakeDashboardProvisioning {
|
||||||
|
mock := &FakeDashboardProvisioning{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
@ -10,10 +10,12 @@ type FakeDashboardService struct {
|
|||||||
DashboardService
|
DashboardService
|
||||||
|
|
||||||
SaveDashboardResult *models.Dashboard
|
SaveDashboardResult *models.Dashboard
|
||||||
SaveDashboardError error
|
|
||||||
SavedDashboards []*SaveDashboardDTO
|
SavedDashboards []*SaveDashboardDTO
|
||||||
ProvisionedDashData *models.DashboardProvisioning
|
ProvisionedDashData *models.DashboardProvisioning
|
||||||
|
|
||||||
|
PublicDashboardConfigResult *models.PublicDashboardConfig
|
||||||
|
PublicDashboardConfigError error
|
||||||
|
SaveDashboardError error
|
||||||
GetDashboardFn func(ctx context.Context, cmd *models.GetDashboardQuery) error
|
GetDashboardFn func(ctx context.Context, cmd *models.GetDashboardQuery) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +29,14 @@ func (s *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashb
|
|||||||
return s.SaveDashboardResult, s.SaveDashboardError
|
return s.SaveDashboardResult, s.SaveDashboardError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
|
||||||
|
return s.PublicDashboardConfigResult, s.PublicDashboardConfigError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) {
|
||||||
|
return s.PublicDashboardConfigResult, s.PublicDashboardConfigError
|
||||||
|
}
|
||||||
|
|
||||||
func (s *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
func (s *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||||
return s.SaveDashboard(ctx, dto, true)
|
return s.SaveDashboard(ctx, dto, true)
|
||||||
}
|
}
|
||||||
|
@ -187,6 +187,47 @@ func (d *DashboardStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models
|
|||||||
return cmd.Result, err
|
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 {
|
func (d *DashboardStore) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error {
|
||||||
return d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
return d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
// delete existing items
|
// delete existing items
|
||||||
|
@ -210,6 +210,29 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dash
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPublicDashboardConfig provides a mock function with given fields: dashboardUid
|
||||||
|
func (_m *FakeDashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) {
|
||||||
|
ret := _m.Called(dashboardUid)
|
||||||
|
|
||||||
|
var r0 *models.PublicDashboardConfig
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *models.PublicDashboardConfig); ok {
|
||||||
|
r0 = rf(dashboardUid)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.PublicDashboardConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(dashboardUid)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// SaveAlerts provides a mock function with given fields: ctx, dashID, alerts
|
// SaveAlerts provides a mock function with given fields: ctx, dashID, alerts
|
||||||
func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error {
|
func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error {
|
||||||
ret := _m.Called(ctx, dashID, alerts)
|
ret := _m.Called(ctx, dashID, alerts)
|
||||||
@ -270,6 +293,29 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(cmd models.SaveDashboardC
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SavePublicDashboardConfig provides a mock function with given fields: cmd
|
||||||
|
func (_m *FakeDashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) {
|
||||||
|
ret := _m.Called(cmd)
|
||||||
|
|
||||||
|
var r0 *models.PublicDashboardConfig
|
||||||
|
if rf, ok := ret.Get(0).(func(models.SavePublicDashboardConfigCommand) *models.PublicDashboardConfig); ok {
|
||||||
|
r0 = rf(cmd)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.PublicDashboardConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(models.SavePublicDashboardConfigCommand) error); ok {
|
||||||
|
r1 = rf(cmd)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// UnprovisionDashboard provides a mock function with given fields: ctx, id
|
// UnprovisionDashboard provides a mock function with given fields: ctx, id
|
||||||
func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error {
|
func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error {
|
||||||
ret := _m.Called(ctx, id)
|
ret := _m.Called(ctx, id)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
// Code generated by mockery v2.12.1. DO NOT EDIT.
|
||||||
|
|
||||||
package dashboards
|
package dashboards
|
||||||
|
|
||||||
@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
models "github.com/grafana/grafana/pkg/models"
|
models "github.com/grafana/grafana/pkg/models"
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
testing "testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FakeFolderService is an autogenerated mock type for the FolderService type
|
// FakeFolderService is an autogenerated mock type for the FolderService type
|
||||||
@ -179,3 +181,13 @@ func (_m *FakeFolderService) UpdateFolder(ctx context.Context, user *models.Sign
|
|||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFakeFolderService creates a new instance of FakeFolderService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewFakeFolderService(t testing.TB) *FakeFolderService {
|
||||||
|
mock := &FakeFolderService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
// Code generated by mockery v2.12.1. DO NOT EDIT.
|
||||||
|
|
||||||
package dashboards
|
package dashboards
|
||||||
|
|
||||||
@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
models "github.com/grafana/grafana/pkg/models"
|
models "github.com/grafana/grafana/pkg/models"
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
testing "testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FakeFolderStore is an autogenerated mock type for the FolderStore type
|
// FakeFolderStore is an autogenerated mock type for the FolderStore type
|
||||||
@ -82,3 +84,13 @@ func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid
|
|||||||
|
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFakeFolderStore creates a new instance of FakeFolderStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewFakeFolderStore(t testing.TB) *FakeFolderStore {
|
||||||
|
mock := &FakeFolderStore{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
@ -14,3 +14,9 @@ type SaveDashboardDTO struct {
|
|||||||
Overwrite bool
|
Overwrite bool
|
||||||
Dashboard *models.Dashboard
|
Dashboard *models.Dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SavePublicDashboardConfigDTO struct {
|
||||||
|
Uid string
|
||||||
|
OrgId int64
|
||||||
|
PublicDashboardConfig models.PublicDashboardConfig
|
||||||
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
@ -342,6 +341,33 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *m.SaveDa
|
|||||||
return dash, nil
|
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 *m.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
|
// 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.
|
// 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 {
|
func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error {
|
||||||
|
@ -230,4 +230,8 @@ func addDashboardMigration(mg *Migrator) {
|
|||||||
Cols: []string{"is_folder"},
|
Cols: []string{"is_folder"},
|
||||||
Type: IndexType,
|
Type: IndexType,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Add isPublic for dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
||||||
|
Name: "is_public", Type: DB_Bool, Nullable: false, Default: "0",
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
@ -3992,60 +3992,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
<<<<<<< HEAD
|
|
||||||
"/dashboards/uid/{uid}/restore": {
|
|
||||||
"post": {
|
|
||||||
"tags": ["dashboard_versions"],
|
|
||||||
"summary": "Restore a dashboard to a given dashboard version using UID.",
|
|
||||||
"operationId": "restoreDashboardVersionByUID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "UID",
|
|
||||||
"name": "uid",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/RestoreDashboardVersionCommand"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"$ref": "#/responses/postDashboardResponse"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"$ref": "#/responses/unauthorisedError"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"$ref": "#/responses/forbiddenError"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"$ref": "#/responses/notFoundError"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"$ref": "#/responses/internalServerError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/dashboards/uid/{uid}/versions": {
|
|
||||||
"get": {
|
|
||||||
"tags": ["dashboard_versions"],
|
|
||||||
"summary": "Gets all existing versions for the dashboard using UID.",
|
|
||||||
"operationId": "getDashboardVersionsByUID",
|
|
||||||
=======
|
|
||||||
"/dashboards/uid/{uid}/versions/{DashboardVersionID}": {
|
"/dashboards/uid/{uid}/versions/{DashboardVersionID}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["dashboard_versions"],
|
"tags": ["dashboard_versions"],
|
||||||
"summary": "Get a specific dashboard version using UID.",
|
"summary": "Get a specific dashboard version using UID.",
|
||||||
"operationId": "getDashboardVersionByUID",
|
"operationId": "getDashboardVersionByUID",
|
||||||
>>>>>>> main
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -4057,35 +4008,14 @@
|
|||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
<<<<<<< HEAD
|
|
||||||
"default": 0,
|
|
||||||
"x-go-name": "Limit",
|
|
||||||
"description": "Maximum number of results to return",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"default": 0,
|
|
||||||
"x-go-name": "Start",
|
|
||||||
"description": "Version to start from when returning queries",
|
|
||||||
"name": "start",
|
|
||||||
"in": "query"
|
|
||||||
=======
|
|
||||||
"name": "DashboardVersionID",
|
"name": "DashboardVersionID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
>>>>>>> main
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
<<<<<<< HEAD
|
|
||||||
"$ref": "#/responses/dashboardVersionsResponse"
|
|
||||||
=======
|
|
||||||
"$ref": "#/responses/dashboardVersionResponse"
|
"$ref": "#/responses/dashboardVersionResponse"
|
||||||
>>>>>>> main
|
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"$ref": "#/responses/unauthorisedError"
|
"$ref": "#/responses/unauthorisedError"
|
||||||
|
@ -3099,7 +3099,9 @@
|
|||||||
"get": {
|
"get": {
|
||||||
"tags": ["dashboard_versions"],
|
"tags": ["dashboard_versions"],
|
||||||
"summary": "Gets all existing versions for the dashboard using UID.",
|
"summary": "Gets all existing versions for the dashboard using UID.",
|
||||||
"operationId": "getDashboardVersionsByUID",
|
"operationId": "getDashboardVersionsByUID"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/dashboards/uid/{uid}/versions/{DashboardVersionID}": {
|
"/dashboards/uid/{uid}/versions/{DashboardVersionID}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["dashboard_versions"],
|
"tags": ["dashboard_versions"],
|
||||||
@ -3116,35 +3118,14 @@
|
|||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
<<<<<<< HEAD
|
|
||||||
"default": 0,
|
|
||||||
"x-go-name": "Limit",
|
|
||||||
"description": "Maximum number of results to return",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"default": 0,
|
|
||||||
"x-go-name": "Start",
|
|
||||||
"description": "Version to start from when returning queries",
|
|
||||||
"name": "start",
|
|
||||||
"in": "query"
|
|
||||||
=======
|
|
||||||
"name": "DashboardVersionID",
|
"name": "DashboardVersionID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
>>>>>>> main
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
<<<<<<< HEAD
|
|
||||||
"$ref": "#/responses/dashboardVersionsResponse"
|
|
||||||
=======
|
|
||||||
"$ref": "#/responses/dashboardVersionResponse"
|
"$ref": "#/responses/dashboardVersionResponse"
|
||||||
>>>>>>> main
|
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"$ref": "#/responses/unauthorisedError"
|
"$ref": "#/responses/unauthorisedError"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
|
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
|
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
|
||||||
@ -9,6 +10,7 @@ import { ShareEmbed } from './ShareEmbed';
|
|||||||
import { ShareExport } from './ShareExport';
|
import { ShareExport } from './ShareExport';
|
||||||
import { ShareLibraryPanel } from './ShareLibraryPanel';
|
import { ShareLibraryPanel } from './ShareLibraryPanel';
|
||||||
import { ShareLink } from './ShareLink';
|
import { ShareLink } from './ShareLink';
|
||||||
|
import { SharePublicDashboard } from './SharePublicDashboard';
|
||||||
import { ShareSnapshot } from './ShareSnapshot';
|
import { ShareSnapshot } from './ShareSnapshot';
|
||||||
import { ShareModalTabModel } from './types';
|
import { ShareModalTabModel } from './types';
|
||||||
|
|
||||||
@ -52,6 +54,10 @@ function getTabs(props: Props) {
|
|||||||
tabs.push(...customDashboardTabs);
|
tabs.push(...customDashboardTabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Boolean(config.featureToggles['publicDashboards'])) {
|
||||||
|
tabs.push({ label: 'Public Dashboard', value: 'share', component: SharePublicDashboard });
|
||||||
|
}
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
|
|
||||||
|
import { ShareModal } from './ShareModal';
|
||||||
|
|
||||||
|
jest.mock('app/core/core', () => {
|
||||||
|
return {
|
||||||
|
contextSrv: {
|
||||||
|
hasPermission: () => true,
|
||||||
|
},
|
||||||
|
appEvents: {
|
||||||
|
subscribe: () => {
|
||||||
|
return {
|
||||||
|
unsubscribe: () => {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
emit: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SharePublic', () => {
|
||||||
|
let originalBootData: any;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
originalBootData = config.bootData;
|
||||||
|
config.appUrl = 'http://dashboards.grafana.com/';
|
||||||
|
|
||||||
|
config.bootData = {
|
||||||
|
user: {
|
||||||
|
orgId: 1,
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
config.bootData = originalBootData;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render share panel when public dashboards feature is disabled', () => {
|
||||||
|
const mockDashboard = new DashboardModel({
|
||||||
|
uid: 'mockDashboardUid',
|
||||||
|
});
|
||||||
|
const mockPanel = new PanelModel({
|
||||||
|
id: 'mockPanelId',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||||
|
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public Dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders share panel when public dashboards feature is enabled', async () => {
|
||||||
|
config.featureToggles.publicDashboards = true;
|
||||||
|
const mockDashboard = new DashboardModel({
|
||||||
|
uid: 'mockDashboardUid',
|
||||||
|
});
|
||||||
|
const mockPanel = new PanelModel({
|
||||||
|
id: 'mockPanelId',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
|
||||||
|
|
||||||
|
await waitFor(() => screen.getByText('Link'));
|
||||||
|
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||||
|
expect(screen.getByRole('tablist')).toHaveTextContent('Public Dashboard');
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Public Dashboard'));
|
||||||
|
|
||||||
|
await waitFor(() => screen.getByText('Enabled'));
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,69 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { Button, Field, Switch } from '@grafana/ui';
|
||||||
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { dispatch } from 'app/store/store';
|
||||||
|
|
||||||
|
import {
|
||||||
|
dashboardCanBePublic,
|
||||||
|
getPublicDashboardConfig,
|
||||||
|
savePublicDashboardConfig,
|
||||||
|
PublicDashboardConfig,
|
||||||
|
} from './SharePublicDashboardUtils';
|
||||||
|
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;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getPublicDashboardConfig(dashboardUid)
|
||||||
|
.then((pdc: PublicDashboardConfig) => {
|
||||||
|
setPublicDashboardConfig(pdc);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(notifyApp(createErrorNotification('Failed to retrieve public dashboard config')));
|
||||||
|
});
|
||||||
|
}, [dashboardUid]);
|
||||||
|
|
||||||
|
const onSavePublicConfig = () => {
|
||||||
|
// verify dashboard can be public
|
||||||
|
if (!dashboardCanBePublic(props.dashboard)) {
|
||||||
|
dispatch(notifyApp(createErrorNotification('This dashboard cannot be made public')));
|
||||||
|
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')));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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)}
|
||||||
|
value={publicDashboardConfig?.isPublic}
|
||||||
|
onChange={() =>
|
||||||
|
setPublicDashboardConfig((state) => {
|
||||||
|
return { ...state, isPublic: !state.isPublic };
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Button onClick={onSavePublicConfig}>Save Sharing Configuration</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,17 @@
|
|||||||
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
|
||||||
|
import { dashboardCanBePublic } from './SharePublicDashboardUtils';
|
||||||
|
|
||||||
|
describe('dashboardCanBePublic', () => {
|
||||||
|
it('can be public with no template variables', () => {
|
||||||
|
//@ts-ignore
|
||||||
|
const dashboard: DashboardModel = { templating: { list: [] } };
|
||||||
|
expect(dashboardCanBePublic(dashboard)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot be public with template variables', () => {
|
||||||
|
//@ts-ignore
|
||||||
|
const dashboard: DashboardModel = { templating: { list: [{}] } };
|
||||||
|
expect(dashboardCanBePublic(dashboard)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,21 @@
|
|||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
|
||||||
|
export interface PublicDashboardConfig {
|
||||||
|
isPublic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardCanBePublic = (dashboard: DashboardModel): boolean => {
|
||||||
|
return dashboard?.templating?.list.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPublicDashboardConfig = async (dashboardUid: string) => {
|
||||||
|
const url = `/api/dashboards/uid/${dashboardUid}/public-config`;
|
||||||
|
return getBackendSrv().get(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const savePublicDashboardConfig = async (dashboardUid: string, conf: PublicDashboardConfig) => {
|
||||||
|
const payload = { isPublic: conf.isPublic };
|
||||||
|
const url = `/api/dashboards/uid/${dashboardUid}/public-config`;
|
||||||
|
return getBackendSrv().post(url, payload);
|
||||||
|
};
|
@ -38,6 +38,7 @@ export interface DashboardMeta {
|
|||||||
fromFile?: boolean;
|
fromFile?: boolean;
|
||||||
hasUnsavedFolderChange?: boolean;
|
hasUnsavedFolderChange?: boolean;
|
||||||
annotationsPermissions?: AnnotationsPermissions;
|
annotationsPermissions?: AnnotationsPermissions;
|
||||||
|
isPublic?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnotationActions {
|
export interface AnnotationActions {
|
||||||
|
Reference in New Issue
Block a user