mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 18:01:40 -06:00
Provisioning: Do not allow deletion of provisioned dashboards (#16211)
* Unprovision dashboard in case of DisableDeletion = true * Rename command struct * Handle removed provision files * Allow html in confirm-modal * Do not show confirm button without onConfirm * Show dialog on deleting provisioned dashboard * Changed DeleteDashboard to DeleteProvisionedDashboard * Remove unreachable return * Add provisioned checks to API * Remove filter func * Fix and add tests for deleting dashboards * Change delete confirm text * Added and used pkg/errors for error wrapping
This commit is contained in:
parent
9f007137b3
commit
2d7fc55df7
2
Gopkg.lock
generated
2
Gopkg.lock
generated
@ -885,6 +885,7 @@
|
|||||||
"github.com/aws/aws-sdk-go/service/sts",
|
"github.com/aws/aws-sdk-go/service/sts",
|
||||||
"github.com/benbjohnson/clock",
|
"github.com/benbjohnson/clock",
|
||||||
"github.com/bmizerany/assert",
|
"github.com/bmizerany/assert",
|
||||||
|
"github.com/bradfitz/gomemcache/memcache",
|
||||||
"github.com/codegangsta/cli",
|
"github.com/codegangsta/cli",
|
||||||
"github.com/davecgh/go-spew/spew",
|
"github.com/davecgh/go-spew/spew",
|
||||||
"github.com/denisenkom/go-mssqldb",
|
"github.com/denisenkom/go-mssqldb",
|
||||||
@ -937,6 +938,7 @@
|
|||||||
"gopkg.in/ldap.v3",
|
"gopkg.in/ldap.v3",
|
||||||
"gopkg.in/macaron.v1",
|
"gopkg.in/macaron.v1",
|
||||||
"gopkg.in/mail.v2",
|
"gopkg.in/mail.v2",
|
||||||
|
"gopkg.in/redis.v2",
|
||||||
"gopkg.in/square/go-jose.v2",
|
"gopkg.in/square/go-jose.v2",
|
||||||
"gopkg.in/yaml.v2",
|
"gopkg.in/yaml.v2",
|
||||||
]
|
]
|
||||||
|
@ -215,3 +215,7 @@ ignored = [
|
|||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "gopkg.in/ldap.v3"
|
name = "gopkg.in/ldap.v3"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/pkg/errors"
|
||||||
|
version = "0.8.0"
|
||||||
|
@ -287,7 +287,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
dashboardRoute.Delete("/uid/:uid", Wrap(DeleteDashboardByUID))
|
dashboardRoute.Delete("/uid/:uid", Wrap(DeleteDashboardByUID))
|
||||||
|
|
||||||
dashboardRoute.Get("/db/:slug", Wrap(GetDashboard))
|
dashboardRoute.Get("/db/:slug", Wrap(GetDashboard))
|
||||||
dashboardRoute.Delete("/db/:slug", Wrap(DeleteDashboard))
|
dashboardRoute.Delete("/db/:slug", Wrap(DeleteDashboardBySlug))
|
||||||
|
|
||||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
|
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*m.Dash
|
|||||||
return query.Result, nil
|
return query.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDashboard(c *m.ReqContext) Response {
|
func DeleteDashboardBySlug(c *m.ReqContext) Response {
|
||||||
query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
|
query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
|
||||||
|
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
@ -164,29 +164,15 @@ func DeleteDashboard(c *m.ReqContext) Response {
|
|||||||
return JSON(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
|
return JSON(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "")
|
return deleteDashboard(c)
|
||||||
if rsp != nil {
|
|
||||||
return rsp
|
|
||||||
}
|
|
||||||
|
|
||||||
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
|
|
||||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
|
||||||
return dashboardGuardianResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
|
||||||
return Error(500, "Failed to delete dashboard", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON(200, util.DynMap{
|
|
||||||
"title": dash.Title,
|
|
||||||
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDashboardByUID(c *m.ReqContext) Response {
|
func DeleteDashboardByUID(c *m.ReqContext) Response {
|
||||||
dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid"))
|
return deleteDashboard(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteDashboard(c *m.ReqContext) Response {
|
||||||
|
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid"))
|
||||||
if rsp != nil {
|
if rsp != nil {
|
||||||
return rsp
|
return rsp
|
||||||
}
|
}
|
||||||
@ -196,8 +182,10 @@ func DeleteDashboardByUID(c *m.ReqContext) Response {
|
|||||||
return dashboardGuardianResponse(err)
|
return dashboardGuardianResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
|
err := dashboards.NewService().DeleteDashboard(dash.Id, c.OrgId)
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err == m.ErrDashboardCannotDeleteProvisionedDashboard {
|
||||||
|
return Error(400, "Dashboard cannot be deleted because it was provisioned", err)
|
||||||
|
} else if err != nil {
|
||||||
return Error(500, "Failed to delete dashboard", err)
|
return Error(500, "Failed to delete dashboard", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 403)
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
|
||||||
Convey("Should lookup dashboard by slug", func() {
|
Convey("Should lookup dashboard by slug", func() {
|
||||||
@ -162,7 +162,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 200)
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
|
||||||
Convey("Should lookup dashboard by slug", func() {
|
Convey("Should lookup dashboard by slug", func() {
|
||||||
@ -273,7 +273,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 403)
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
|
||||||
Convey("Should lookup dashboard by slug", func() {
|
Convey("Should lookup dashboard by slug", func() {
|
||||||
@ -331,7 +331,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 403)
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
|
||||||
Convey("Should lookup dashboard by slug", func() {
|
Convey("Should lookup dashboard by slug", func() {
|
||||||
@ -400,7 +400,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 200)
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
|
||||||
Convey("Should lookup dashboard by slug", func() {
|
Convey("Should lookup dashboard by slug", func() {
|
||||||
@ -470,7 +470,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 403)
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
|
||||||
Convey("Should lookup dashboard by slug", func() {
|
Convey("Should lookup dashboard by slug", func() {
|
||||||
@ -529,7 +529,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 200)
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
|
||||||
Convey("Should lookup dashboard by slug", func() {
|
Convey("Should lookup dashboard by slug", func() {
|
||||||
@ -596,7 +596,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 403)
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
|
||||||
Convey("Should lookup dashboard by slug", func() {
|
Convey("Should lookup dashboard by slug", func() {
|
||||||
@ -650,7 +650,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
role := m.ROLE_EDITOR
|
role := m.ROLE_EDITOR
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||||
CallDeleteDashboard(sc)
|
CallDeleteDashboardBySlug(sc)
|
||||||
|
|
||||||
Convey("Should result in 412 Precondition failed", func() {
|
Convey("Should result in 412 Precondition failed", func() {
|
||||||
So(sc.resp.Code, ShouldEqual, 412)
|
So(sc.resp.Code, ShouldEqual, 412)
|
||||||
@ -897,6 +897,50 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
So(dto.Message, ShouldEqual, "Restored from version 1")
|
So(dto.Message, ShouldEqual, "Restored from version 1")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Given provisioned dashboard", t, func() {
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
|
||||||
|
query.Result = []*m.Dashboard{{}}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||||
|
query.Result = &m.Dashboard{Id: 1}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||||
|
query.Result = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = []*m.DashboardAclInfoDTO{
|
||||||
|
{OrgId: TestOrgID, DashboardId: 1, UserId: TestUserID, Permission: m.PERMISSION_EDIT},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboardBySlug(sc)
|
||||||
|
|
||||||
|
Convey("Should result in 400", func() {
|
||||||
|
So(sc.resp.Code, ShouldEqual, 400)
|
||||||
|
result := sc.ToJSON()
|
||||||
|
So(result.Get("error").MustString(), ShouldEqual, m.ErrDashboardCannotDeleteProvisionedDashboard.Error())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboardByUID(sc)
|
||||||
|
|
||||||
|
Convey("Should result in 400", func() {
|
||||||
|
So(sc.resp.Code, ShouldEqual, 400)
|
||||||
|
result := sc.ToJSON()
|
||||||
|
So(result.Get("error").MustString(), ShouldEqual, m.ErrDashboardCannotDeleteProvisionedDashboard.Error())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||||
@ -936,12 +980,12 @@ func CallGetDashboardVersions(sc *scenarioContext) {
|
|||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallDeleteDashboard(sc *scenarioContext) {
|
func CallDeleteDashboardBySlug(sc *scenarioContext) {
|
||||||
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
|
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.handlerFunc = DeleteDashboard
|
sc.handlerFunc = DeleteDashboardBySlug
|
||||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,25 +13,26 @@ import (
|
|||||||
|
|
||||||
// Typed errors
|
// Typed errors
|
||||||
var (
|
var (
|
||||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||||
ErrDashboardFolderNotFound = errors.New("Folder not found")
|
ErrDashboardFolderNotFound = errors.New("Folder not found")
|
||||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||||
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
|
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
|
||||||
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
|
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
|
||||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||||
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
||||||
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
||||||
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
||||||
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
|
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
|
||||||
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
|
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
|
||||||
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
|
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
|
||||||
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
|
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
|
||||||
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
|
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
|
||||||
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
|
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
|
||||||
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
|
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
|
||||||
ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboard")
|
ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboard")
|
||||||
RootFolderName = "General"
|
ErrDashboardCannotDeleteProvisionedDashboard = errors.New("provisioned dashboard cannot be deleted")
|
||||||
|
RootFolderName = "General"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdatePluginDashboardError struct {
|
type UpdatePluginDashboardError struct {
|
||||||
@ -356,3 +357,7 @@ type GetDashboardRefByIdQuery struct {
|
|||||||
Id int64
|
Id int64
|
||||||
Result *DashboardRef
|
Result *DashboardRef
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UnprovisionDashboardCommand struct {
|
||||||
|
Id int64
|
||||||
|
}
|
||||||
|
@ -9,12 +9,14 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DashboardService service for operating on dashboards
|
// DashboardService service for operating on dashboards
|
||||||
type DashboardService interface {
|
type DashboardService interface {
|
||||||
SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||||
ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||||
|
DeleteDashboard(dashboardId int64, orgId int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardProvisioningService service for operating on provisioned dashboards
|
// DashboardProvisioningService service for operating on provisioned dashboards
|
||||||
@ -22,6 +24,8 @@ type DashboardProvisioningService interface {
|
|||||||
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
|
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
|
||||||
SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
|
SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
|
||||||
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
||||||
|
UnprovisionDashboard(dashboardId int64) error
|
||||||
|
DeleteProvisionedDashboard(dashboardId int64, orgId int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService factory for creating a new dashboard service
|
// NewService factory for creating a new dashboard service
|
||||||
@ -241,6 +245,33 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da
|
|||||||
return cmd.Result, nil
|
return cmd.Result, 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(dashboardId int64, orgId int64) error {
|
||||||
|
return dr.deleteDashboard(dashboardId, orgId, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProvisionedDashboard removes dashboard from the DB even if it is provisioned.
|
||||||
|
func (dr *dashboardServiceImpl) DeleteProvisionedDashboard(dashboardId int64, orgId int64) error {
|
||||||
|
return dr.deleteDashboard(dashboardId, orgId, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *dashboardServiceImpl) deleteDashboard(dashboardId int64, orgId int64, validateProvisionedDashboard bool) error {
|
||||||
|
if validateProvisionedDashboard {
|
||||||
|
isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dashboardId}
|
||||||
|
err := bus.Dispatch(isDashboardProvisioned)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error while checking if dashboard is provisioned")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDashboardProvisioned.Result {
|
||||||
|
return models.ErrDashboardCannotDeleteProvisionedDashboard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd := &models.DeleteDashboardCommand{OrgId: orgId, Id: dashboardId}
|
||||||
|
return bus.Dispatch(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||||
cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
|
cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -255,6 +286,13 @@ func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.
|
|||||||
return cmd.Result, nil
|
return cmd.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnprovisionDashboard removes info about dashboard being provisioned. Used after provisioning configs are changed
|
||||||
|
// and provisioned dashboards are left behind but not deleted.
|
||||||
|
func (dr *dashboardServiceImpl) UnprovisionDashboard(dashboardId int64) error {
|
||||||
|
cmd := &models.UnprovisionDashboardCommand{Id: dashboardId}
|
||||||
|
return bus.Dispatch(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
type FakeDashboardService struct {
|
type FakeDashboardService struct {
|
||||||
SaveDashboardResult *models.Dashboard
|
SaveDashboardResult *models.Dashboard
|
||||||
SaveDashboardError error
|
SaveDashboardError error
|
||||||
@ -275,6 +313,16 @@ func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.D
|
|||||||
return s.SaveDashboard(dto)
|
return s.SaveDashboard(dto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FakeDashboardService) DeleteDashboard(dashboardId int64, orgId int64) error {
|
||||||
|
for index, dash := range s.SavedDashboards {
|
||||||
|
if dash.Dashboard.Id == dashboardId && dash.OrgId == orgId {
|
||||||
|
s.SavedDashboards = append(s.SavedDashboards[:index], s.SavedDashboards[index+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func MockDashboardService(mock *FakeDashboardService) {
|
func MockDashboardService(mock *FakeDashboardService) {
|
||||||
NewService = func() DashboardService {
|
NewService = func() DashboardService {
|
||||||
return mock
|
return mock
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package dashboards
|
package dashboards
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
@ -200,8 +199,61 @@ func TestDashboardService(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Given provisioned dashboard", func() {
|
||||||
|
result := setupDeleteHandlers(true)
|
||||||
|
|
||||||
|
Convey("DeleteProvisionedDashboard should delete it", func() {
|
||||||
|
err := service.DeleteProvisionedDashboard(1, 1)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(result.deleteWasCalled, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("DeleteDashboard should fail to delete it", func() {
|
||||||
|
err := service.DeleteDashboard(1, 1)
|
||||||
|
So(err, ShouldEqual, models.ErrDashboardCannotDeleteProvisionedDashboard)
|
||||||
|
So(result.deleteWasCalled, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given non provisioned dashboard", func() {
|
||||||
|
result := setupDeleteHandlers(false)
|
||||||
|
|
||||||
|
Convey("DeleteProvisionedDashboard should delete it", func() {
|
||||||
|
err := service.DeleteProvisionedDashboard(1, 1)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(result.deleteWasCalled, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("DeleteDashboard should delete it", func() {
|
||||||
|
err := service.DeleteDashboard(1, 1)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(result.deleteWasCalled, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Reset(func() {
|
Reset(func() {
|
||||||
guardian.New = origNewDashboardGuardian
|
guardian.New = origNewDashboardGuardian
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
deleteWasCalled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDeleteHandlers(provisioned bool) *Result {
|
||||||
|
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||||
|
cmd.Result = provisioned
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
result := &Result{}
|
||||||
|
bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error {
|
||||||
|
So(cmd.Id, ShouldEqual, 1)
|
||||||
|
So(cmd.OrgId, ShouldEqual, 1)
|
||||||
|
result.deleteWasCalled = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -25,10 +25,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type fileReader struct {
|
type fileReader struct {
|
||||||
Cfg *DashboardsAsConfig
|
Cfg *DashboardsAsConfig
|
||||||
Path string
|
Path string
|
||||||
log log.Logger
|
log log.Logger
|
||||||
dashboardService dashboards.DashboardProvisioningService
|
dashboardProvisioningService dashboards.DashboardProvisioningService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
|
func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
|
||||||
@ -44,10 +44,10 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &fileReader{
|
return &fileReader{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Path: path,
|
Path: path,
|
||||||
log: log,
|
log: log,
|
||||||
dashboardService: dashboards.NewProvisioningService(),
|
dashboardProvisioningService: dashboards.NewProvisioningService(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,12 +86,12 @@ func (fr *fileReader) startWalkingDisk() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardService)
|
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardProvisioningService)
|
||||||
if err != nil && err != ErrFolderNameMissing {
|
if err != nil && err != ErrFolderNameMissing {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardService, fr.Cfg.Name)
|
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardProvisioningService, fr.Cfg.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ func (fr *fileReader) startWalkingDisk() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fr.deleteDashboardIfFileIsMissing(provisionedDashboardRefs, filesFoundOnDisk)
|
fr.handleMissingDashboardFiles(provisionedDashboardRefs, filesFoundOnDisk)
|
||||||
|
|
||||||
sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name)
|
sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name)
|
||||||
|
|
||||||
@ -119,11 +119,7 @@ func (fr *fileReader) startWalkingDisk() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
|
func (fr *fileReader) handleMissingDashboardFiles(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
|
||||||
if fr.Cfg.DisableDeletion {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// find dashboards to delete since json file is missing
|
// find dashboards to delete since json file is missing
|
||||||
var dashboardToDelete []int64
|
var dashboardToDelete []int64
|
||||||
for path, provisioningData := range provisionedDashboardRefs {
|
for path, provisioningData := range provisionedDashboardRefs {
|
||||||
@ -132,13 +128,25 @@ func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs ma
|
|||||||
dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
|
dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// delete dashboard that are missing json file
|
|
||||||
for _, dashboardId := range dashboardToDelete {
|
if fr.Cfg.DisableDeletion {
|
||||||
fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
|
// If deletion is disabled for the provisioner we just remove provisioning metadata about the dashboard
|
||||||
cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
|
// so afterwards the dashboard is considered unprovisioned.
|
||||||
err := bus.Dispatch(cmd)
|
for _, dashboardId := range dashboardToDelete {
|
||||||
if err != nil {
|
fr.log.Debug("unprovisioning provisioned dashboard. missing on disk", "id", dashboardId)
|
||||||
fr.log.Error("failed to delete dashboard", "id", cmd.Id, "error", err)
|
err := fr.dashboardProvisioningService.UnprovisionDashboard(dashboardId)
|
||||||
|
if err != nil {
|
||||||
|
fr.log.Error("failed to unprovision dashboard", "dashboard_id", dashboardId, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// delete dashboard that are missing json file
|
||||||
|
for _, dashboardId := range dashboardToDelete {
|
||||||
|
fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
|
||||||
|
err := fr.dashboardProvisioningService.DeleteProvisionedDashboard(dashboardId, fr.Cfg.OrgId)
|
||||||
|
if err != nil {
|
||||||
|
fr.log.Error("failed to delete dashboard", "id", dashboardId, "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,7 +197,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
|||||||
CheckSum: jsonFile.checkSum,
|
CheckSum: jsonFile.checkSum,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
|
_, err = fr.dashboardProvisioningService.SaveProvisionedDashboard(dash, dp)
|
||||||
return provisioningMetadata, err
|
return provisioningMetadata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package dashboards
|
package dashboards
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -20,6 +22,7 @@ var (
|
|||||||
brokenDashboards = "testdata/test-dashboards/broken-dashboards"
|
brokenDashboards = "testdata/test-dashboards/broken-dashboards"
|
||||||
oneDashboard = "testdata/test-dashboards/one-dashboard"
|
oneDashboard = "testdata/test-dashboards/one-dashboard"
|
||||||
containingId = "testdata/test-dashboards/containing-id"
|
containingId = "testdata/test-dashboards/containing-id"
|
||||||
|
unprovision = "testdata/test-dashboards/unprovision"
|
||||||
|
|
||||||
fakeService *fakeDashboardProvisioningService
|
fakeService *fakeDashboardProvisioningService
|
||||||
)
|
)
|
||||||
@ -250,6 +253,62 @@ func TestDashboardFileReader(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Given missing dashboard file", func() {
|
||||||
|
cfg := &DashboardsAsConfig{
|
||||||
|
Name: "Default",
|
||||||
|
Type: "file",
|
||||||
|
OrgId: 1,
|
||||||
|
Options: map[string]interface{}{
|
||||||
|
"folder": unprovision,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeService.inserted = []*dashboards.SaveDashboardDTO{
|
||||||
|
{Dashboard: &models.Dashboard{Id: 1}},
|
||||||
|
{Dashboard: &models.Dashboard{Id: 2}},
|
||||||
|
}
|
||||||
|
|
||||||
|
absPath1, err := filepath.Abs(unprovision + "/dashboard1.json")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
// This one does not exist on disc, simulating a deleted file
|
||||||
|
absPath2, err := filepath.Abs(unprovision + "/dashboard2.json")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
fakeService.provisioned = map[string][]*models.DashboardProvisioning{
|
||||||
|
"Default": {
|
||||||
|
{DashboardId: 1, Name: "Default", ExternalId: absPath1},
|
||||||
|
{DashboardId: 2, Name: "Default", ExternalId: absPath2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("Missing dashboard should be unprovisioned if DisableDeletion = true", func() {
|
||||||
|
cfg.DisableDeletion = true
|
||||||
|
|
||||||
|
reader, err := NewDashboardFileReader(cfg, logger)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = reader.startWalkingDisk()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
So(len(fakeService.provisioned["Default"]), ShouldEqual, 1)
|
||||||
|
So(fakeService.provisioned["Default"][0].ExternalId, ShouldEqual, absPath1)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Missing dashboard should be deleted if DisableDeletion = false", func() {
|
||||||
|
reader, err := NewDashboardFileReader(cfg, logger)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = reader.startWalkingDisk()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
So(len(fakeService.provisioned["Default"]), ShouldEqual, 1)
|
||||||
|
So(fakeService.provisioned["Default"][0].ExternalId, ShouldEqual, absPath1)
|
||||||
|
So(len(fakeService.inserted), ShouldEqual, 1)
|
||||||
|
So(fakeService.inserted[0].Dashboard.Id, ShouldEqual, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Reset(func() {
|
Reset(func() {
|
||||||
dashboards.NewProvisioningService = origNewDashboardProvisioningService
|
dashboards.NewProvisioningService = origNewDashboardProvisioningService
|
||||||
})
|
})
|
||||||
@ -310,13 +369,39 @@ func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
||||||
|
// Copy the structs as we need to change them but do not want to alter outside world.
|
||||||
|
var copyProvisioning = &models.DashboardProvisioning{}
|
||||||
|
*copyProvisioning = *provisioning
|
||||||
|
|
||||||
|
var copyDto = &dashboards.SaveDashboardDTO{}
|
||||||
|
*copyDto = *dto
|
||||||
|
|
||||||
|
if copyDto.Dashboard.Id == 0 {
|
||||||
|
copyDto.Dashboard.Id = rand.Int63n(1000000)
|
||||||
|
} else {
|
||||||
|
err := s.DeleteProvisionedDashboard(dto.Dashboard.Id, dto.Dashboard.OrgId)
|
||||||
|
// Lets delete existing so we do not have duplicates
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.inserted = append(s.inserted, dto)
|
s.inserted = append(s.inserted, dto)
|
||||||
|
|
||||||
if _, ok := s.provisioned[provisioning.Name]; !ok {
|
if _, ok := s.provisioned[provisioning.Name]; !ok {
|
||||||
s.provisioned[provisioning.Name] = []*models.DashboardProvisioning{}
|
s.provisioned[provisioning.Name] = []*models.DashboardProvisioning{}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.provisioned[provisioning.Name] = append(s.provisioned[provisioning.Name], provisioning)
|
for _, val := range s.provisioned[provisioning.Name] {
|
||||||
|
if val.DashboardId == dto.Dashboard.Id && val.Name == provisioning.Name {
|
||||||
|
// Do not insert duplicates
|
||||||
|
return dto.Dashboard, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyProvisioning.DashboardId = copyDto.Dashboard.Id
|
||||||
|
|
||||||
|
s.provisioned[provisioning.Name] = append(s.provisioned[provisioning.Name], copyProvisioning)
|
||||||
return dto.Dashboard, nil
|
return dto.Dashboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,6 +410,31 @@ func (s *fakeDashboardProvisioningService) SaveFolderForProvisionedDashboards(dt
|
|||||||
return dto.Dashboard, nil
|
return dto.Dashboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *fakeDashboardProvisioningService) UnprovisionDashboard(dashboardId int64) error {
|
||||||
|
for key, val := range s.provisioned {
|
||||||
|
for index, dashboard := range val {
|
||||||
|
if dashboard.DashboardId == dashboardId {
|
||||||
|
s.provisioned[key] = append(s.provisioned[key][:index], s.provisioned[key][index+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeDashboardProvisioningService) DeleteProvisionedDashboard(dashboardId int64, orgId int64) error {
|
||||||
|
err := s.UnprovisionDashboard(dashboardId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, val := range s.inserted {
|
||||||
|
if val.Dashboard.Id == dashboardId {
|
||||||
|
s.inserted = append(s.inserted[:index], s.inserted[util.MinInt(index+1, len(s.inserted)):]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
|
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
|
||||||
for _, d := range fakeService.getDashboard {
|
for _, d := range fakeService.getDashboard {
|
||||||
if d.Slug == cmd.Slug {
|
if d.Slug == cmd.Slug {
|
||||||
|
172
pkg/services/provisioning/dashboards/testdata/test-dashboards/unprovision/dashboard1.json
vendored
Normal file
172
pkg/services/provisioning/dashboards/testdata/test-dashboards/unprovision/dashboard1.json
vendored
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
{
|
||||||
|
"title": "Grafana1",
|
||||||
|
"tags": [],
|
||||||
|
"style": "dark",
|
||||||
|
"timezone": "browser",
|
||||||
|
"editable": true,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"title": "New row",
|
||||||
|
"height": "150px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"span": 12,
|
||||||
|
"editable": true,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Welcome to"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Welcome to Grafana",
|
||||||
|
"height": "210px",
|
||||||
|
"collapse": false,
|
||||||
|
"editable": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"span": 6,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||||
|
"style": {},
|
||||||
|
"title": "Documentation Links"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"span": 6,
|
||||||
|
"type": "text",
|
||||||
|
"mode": "html",
|
||||||
|
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||||
|
"style": {},
|
||||||
|
"title": "Tips & Shortcuts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "test",
|
||||||
|
"height": "250px",
|
||||||
|
"editable": true,
|
||||||
|
"collapse": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"span": 12,
|
||||||
|
"type": "graph",
|
||||||
|
"x-axis": true,
|
||||||
|
"y-axis": true,
|
||||||
|
"scale": 1,
|
||||||
|
"y_formats": [
|
||||||
|
"short",
|
||||||
|
"short"
|
||||||
|
],
|
||||||
|
"grid": {
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"leftMax": null,
|
||||||
|
"rightMax": null,
|
||||||
|
"leftMin": null,
|
||||||
|
"rightMin": null,
|
||||||
|
"threshold1": null,
|
||||||
|
"threshold2": null,
|
||||||
|
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||||
|
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||||
|
},
|
||||||
|
"resolution": 100,
|
||||||
|
"lines": true,
|
||||||
|
"fill": 1,
|
||||||
|
"linewidth": 2,
|
||||||
|
"dashes": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"spaceLength": 10,
|
||||||
|
"points": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"bars": false,
|
||||||
|
"stack": true,
|
||||||
|
"spyable": true,
|
||||||
|
"options": false,
|
||||||
|
"legend": {
|
||||||
|
"show": true,
|
||||||
|
"values": false,
|
||||||
|
"min": false,
|
||||||
|
"max": false,
|
||||||
|
"current": false,
|
||||||
|
"total": false,
|
||||||
|
"avg": false
|
||||||
|
},
|
||||||
|
"interactive": true,
|
||||||
|
"legend_counts": true,
|
||||||
|
"timezone": "browser",
|
||||||
|
"percentage": false,
|
||||||
|
"nullPointMode": "connected",
|
||||||
|
"steppedLine": false,
|
||||||
|
"tooltip": {
|
||||||
|
"value_type": "cumulative",
|
||||||
|
"query_as_alias": true
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"target": "randomWalk('random walk')",
|
||||||
|
"function": "mean",
|
||||||
|
"column": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliasColors": {},
|
||||||
|
"aliasYAxis": {},
|
||||||
|
"title": "First Graph (click title to edit)",
|
||||||
|
"datasource": "graphite",
|
||||||
|
"renderer": "flot",
|
||||||
|
"annotate": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nav": [
|
||||||
|
{
|
||||||
|
"type": "timepicker",
|
||||||
|
"collapse": false,
|
||||||
|
"enable": true,
|
||||||
|
"status": "Stable",
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
],
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"now": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"version": 5
|
||||||
|
}
|
@ -9,6 +9,7 @@ func init() {
|
|||||||
bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
|
bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
|
||||||
bus.AddHandler("sql", SaveProvisionedDashboard)
|
bus.AddHandler("sql", SaveProvisionedDashboard)
|
||||||
bus.AddHandler("sql", GetProvisionedDataByDashboardId)
|
bus.AddHandler("sql", GetProvisionedDataByDashboardId)
|
||||||
|
bus.AddHandler("sql", UnprovisionDashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardExtras struct {
|
type DashboardExtras struct {
|
||||||
@ -44,11 +45,11 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error
|
|||||||
cmd.DashboardProvisioning.Updated = cmd.Result.Updated.Unix()
|
cmd.DashboardProvisioning.Updated = cmd.Result.Updated.Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveProvionedData(sess, cmd.DashboardProvisioning, cmd.Result)
|
return saveProvisionedData(sess, cmd.DashboardProvisioning, cmd.Result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
|
func saveProvisionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
|
||||||
result := &models.DashboardProvisioning{}
|
result := &models.DashboardProvisioning{}
|
||||||
|
|
||||||
exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, cmd.Name).Get(result)
|
exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, cmd.Name).Get(result)
|
||||||
@ -78,3 +79,12 @@ func GetProvisionedDashboardDataQuery(cmd *models.GetProvisionedDashboardDataQue
|
|||||||
cmd.Result = result
|
cmd.Result = result
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnprovisionDashboard removes row in dashboard_provisioning for the dashboard making it seem as if manually created.
|
||||||
|
// The dashboard will still have `created_by = -1` to see it was not created by any particular user.
|
||||||
|
func UnprovisionDashboard(cmd *models.UnprovisionDashboardCommand) error {
|
||||||
|
if _, err := x.Where("dashboard_id = ?", cmd.Id).Delete(&models.DashboardProvisioning{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -81,7 +81,7 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
|||||||
So(query.Result, ShouldBeFalse)
|
So(query.Result, ShouldBeFalse)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Deleteing folder should delete provision meta data", func() {
|
Convey("Deleting folder should delete provision meta data", func() {
|
||||||
deleteCmd := &models.DeleteDashboardCommand{
|
deleteCmd := &models.DeleteDashboardCommand{
|
||||||
Id: folderCmd.Result.Id,
|
Id: folderCmd.Result.Id,
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
@ -95,6 +95,20 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(query.Result, ShouldBeFalse)
|
So(query.Result, ShouldBeFalse)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("UnprovisionDashboard should delete provisioning metadata", func() {
|
||||||
|
unprovisionCmd := &models.UnprovisionDashboardCommand{
|
||||||
|
Id: dashId,
|
||||||
|
}
|
||||||
|
|
||||||
|
So(UnprovisionDashboard(unprovisionCmd), ShouldBeNil)
|
||||||
|
|
||||||
|
query := &models.IsDashboardProvisionedQuery{DashboardId: dashId}
|
||||||
|
|
||||||
|
err = GetProvisionedDataByDashboardId(query)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(query.Result, ShouldBeFalse)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
17
pkg/util/math.go
Normal file
17
pkg/util/math.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
// MaxInt returns the larger of x or y.
|
||||||
|
func MaxInt(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinInt returns the smaller of x or y.
|
||||||
|
func MinInt(x, y int) int {
|
||||||
|
if x > y {
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
@ -52,11 +52,6 @@ export class UtilSrv {
|
|||||||
showConfirmModal(payload) {
|
showConfirmModal(payload) {
|
||||||
const scope = this.$rootScope.$new();
|
const scope = this.$rootScope.$new();
|
||||||
|
|
||||||
scope.onConfirm = () => {
|
|
||||||
payload.onConfirm();
|
|
||||||
scope.dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
scope.updateConfirmText = value => {
|
scope.updateConfirmText = value => {
|
||||||
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
|
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
|
||||||
};
|
};
|
||||||
@ -64,6 +59,7 @@ export class UtilSrv {
|
|||||||
scope.title = payload.title;
|
scope.title = payload.title;
|
||||||
scope.text = payload.text;
|
scope.text = payload.text;
|
||||||
scope.text2 = payload.text2;
|
scope.text2 = payload.text2;
|
||||||
|
scope.text2htmlBind = payload.text2htmlBind;
|
||||||
scope.confirmText = payload.confirmText;
|
scope.confirmText = payload.confirmText;
|
||||||
|
|
||||||
scope.onConfirm = payload.onConfirm;
|
scope.onConfirm = payload.onConfirm;
|
||||||
|
@ -182,6 +182,24 @@ export class SettingsCtrl {
|
|||||||
let confirmText = '';
|
let confirmText = '';
|
||||||
let text2 = this.dashboard.title;
|
let text2 = this.dashboard.title;
|
||||||
|
|
||||||
|
if (this.dashboard.meta.provisioned) {
|
||||||
|
appEvents.emit('confirm-modal', {
|
||||||
|
title: 'Cannot delete provisioned dashboard',
|
||||||
|
text: `
|
||||||
|
This dashboard is managed by Grafanas provisioning and cannot be deleted. Remove the dashboard from the
|
||||||
|
config file to delete it.
|
||||||
|
`,
|
||||||
|
text2: `
|
||||||
|
<i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank">
|
||||||
|
documentation</a> for more information about provisioning.</i>
|
||||||
|
`,
|
||||||
|
text2htmlBind: true,
|
||||||
|
icon: 'fa-trash',
|
||||||
|
noText: 'OK',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const alerts = _.sumBy(this.dashboard.panels, panel => {
|
const alerts = _.sumBy(this.dashboard.panels, panel => {
|
||||||
return panel.alert ? 1 : 0;
|
return panel.alert ? 1 : 0;
|
||||||
});
|
});
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
|
|
||||||
<div class="confirm-modal-text">
|
<div class="confirm-modal-text">
|
||||||
{{text}}
|
{{text}}
|
||||||
<div class="confirm-modal-text2" ng-show="text2">
|
<div ng-if="text2 && text2htmlBind" class="confirm-modal-text2" ng-bind-html="text2"></div>
|
||||||
{{text2}}
|
<div ng-if="text2 && !text2htmlBind" class="confirm-modal-text2">{{text2}}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-content-confirm-text" ng-if="confirmText">
|
<div class="modal-content-confirm-text" ng-if="confirmText">
|
||||||
@ -27,7 +26,7 @@
|
|||||||
|
|
||||||
<div class="confirm-modal-buttons">
|
<div class="confirm-modal-buttons">
|
||||||
<button ng-show="onAltAction" type="button" class="btn btn-primary" ng-click="dismiss();onAltAction();">{{altActionText}}</button>
|
<button ng-show="onAltAction" type="button" class="btn btn-primary" ng-click="dismiss();onAltAction();">{{altActionText}}</button>
|
||||||
<button type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();" ng-disabled="!confirmTextValid" give-focus="true">{{yesText}}</button>
|
<button ng-show="onConfirm" type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();" ng-disabled="!confirmTextValid" give-focus="true">{{yesText}}</button>
|
||||||
<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
|
<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user