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:
Jeff Levin
2022-05-17 14:11:55 -08:00
committed by GitHub
co-authored by Owen Smallwood Torkel Ödegaard
parent 156e14e296
commit c7f8c2cc73
25 changed files with 607 additions and 108 deletions
+13 -7
View File
@@ -378,14 +378,20 @@ func (hs *HTTPServer) registerRoutes() {
})
})
if hs.ThumbService != nil {
dashboardRoute.Get("/uid/:uid/img/:kind/:theme", hs.ThumbService.GetImage)
if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
dashboardRoute.Post("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.SetImage)
dashboardRoute.Put("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.UpdateThumbnailState)
dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) {
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
dashUidRoute.Get("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetPublicDashboard))
dashUidRoute.Post("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.SavePublicDashboard))
}
}
if hs.ThumbService != nil {
dashUidRoute.Get("/img/:kind/:theme", hs.ThumbService.GetImage)
if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
dashUidRoute.Post("/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.SetImage)
dashUidRoute.Put("/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.UpdateThumbnailState)
}
}
})
dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
+12 -3
View File
@@ -40,6 +40,7 @@ import (
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/searchusers/filters"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/grafana/grafana/pkg/web/webtest"
@@ -332,10 +333,19 @@ func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessContro
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
db := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{})
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, db)
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, db, featuremgmt.WithFeatures())
}
func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore, store sqlstore.Store) accessControlScenarioContext {
func setupHTTPServerWithMockDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, features *featuremgmt.FeatureManager) accessControlScenarioContext {
// Use a new conf
cfg := setting.NewCfg()
db := sqlstore.InitTestDB(t)
db.Cfg = setting.NewCfg()
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, mockstore.NewSQLStoreMock(), features)
}
func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore, store sqlstore.Store, features *featuremgmt.FeatureManager) accessControlScenarioContext {
t.Helper()
if enableAccessControl {
@@ -345,7 +355,6 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
cfg.RBACEnabled = false
db.Cfg.RBACEnabled = false
}
features := featuremgmt.WithFeatures()
var acmock *accesscontrolmock.Mock
+1
View File
@@ -146,6 +146,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
Url: dash.GetUrl(),
FolderTitle: "General",
AnnotationsPermissions: annotationPermissions,
IsPublic: dash.IsPublic,
}
// lookup folder title
+56
View 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
View 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)
}
})
}
}
+1
View File
@@ -33,6 +33,7 @@ type DashboardMeta struct {
Provisioned bool `json:"provisioned"`
ProvisionedExternalId string `json:"provisionedExternalId"`
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
IsPublic bool `json:"isPublic"`
}
type AnnotationPermission struct {
Dashboard AnnotationActions `json:"dashboard"`
+11
View File
@@ -199,11 +199,16 @@ type Dashboard struct {
FolderId int64
IsFolder bool
HasAcl bool
IsPublic bool
Title string
Data *simplejson.Json
}
type PublicDashboardConfig struct {
IsPublic bool `json:"isPublic"`
}
func (d *Dashboard) SetId(id int64) {
d.Id = id
d.Data.Set("id", id)
@@ -411,6 +416,12 @@ type DeleteOrphanedProvisionedDashboardsCommand struct {
ReaderNames []string
}
type SavePublicDashboardConfigCommand struct {
Uid string
OrgId int64
PublicDashboardConfig PublicDashboardConfig
}
//
// QUERIES
//
+11
View File
@@ -6,9 +6,18 @@ import (
"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.
type DashboardService interface {
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)
DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) 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)
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*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
DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error
// 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
@@ -7,6 +7,8 @@ import (
models "github.com/grafana/grafana/pkg/models"
mock "github.com/stretchr/testify/mock"
testing "testing"
)
// FakeDashboardProvisioning is an autogenerated mock type for the DashboardProvisioningService type
@@ -170,3 +172,13 @@ func (_m *FakeDashboardProvisioning) UnprovisionDashboard(ctx context.Context, d
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,11 +10,13 @@ type FakeDashboardService struct {
DashboardService
SaveDashboardResult *models.Dashboard
SaveDashboardError error
SavedDashboards []*SaveDashboardDTO
ProvisionedDashData *models.DashboardProvisioning
GetDashboardFn func(ctx context.Context, cmd *models.GetDashboardQuery) error
PublicDashboardConfigResult *models.PublicDashboardConfig
PublicDashboardConfigError error
SaveDashboardError error
GetDashboardFn func(ctx context.Context, cmd *models.GetDashboardQuery) error
}
func (s *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) {
@@ -27,6 +29,14 @@ func (s *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashb
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) {
return s.SaveDashboard(ctx, dto, true)
}
@@ -187,6 +187,47 @@ 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
+46
View File
@@ -210,6 +210,29 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dash
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
func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error {
ret := _m.Called(ctx, dashID, alerts)
@@ -270,6 +293,29 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(cmd models.SaveDashboardC
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
func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
+13 -1
View File
@@ -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
@@ -7,6 +7,8 @@ import (
models "github.com/grafana/grafana/pkg/models"
mock "github.com/stretchr/testify/mock"
testing "testing"
)
// 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
}
// 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
}
+13 -1
View File
@@ -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
@@ -7,6 +7,8 @@ import (
models "github.com/grafana/grafana/pkg/models"
mock "github.com/stretchr/testify/mock"
testing "testing"
)
// 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
}
// 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
}
+6
View File
@@ -14,3 +14,9 @@ type SaveDashboardDTO struct {
Overwrite bool
Dashboard *models.Dashboard
}
type SavePublicDashboardConfigDTO struct {
Uid string
OrgId int64
PublicDashboardConfig models.PublicDashboardConfig
}
@@ -7,7 +7,6 @@ import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -342,6 +341,33 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *m.SaveDa
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
// 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 {
@@ -230,4 +230,8 @@ func addDashboardMigration(mg *Migrator) {
Cols: []string{"is_folder"},
Type: IndexType,
}))
mg.AddMigration("Add isPublic for dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "is_public", Type: DB_Bool, Nullable: false, Default: "0",
}))
}