mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #10739 from grafana/10630_folder_api
New folder and permissions API
This commit is contained in:
commit
a34acdda1a
@ -246,6 +246,24 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
|
apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||||
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||||
|
|
||||||
|
// Folders
|
||||||
|
apiRoute.Group("/folders", func(folderRoute RouteRegister) {
|
||||||
|
folderRoute.Get("/", wrap(GetFolders))
|
||||||
|
folderRoute.Get("/id/:id", wrap(GetFolderById))
|
||||||
|
folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
|
||||||
|
|
||||||
|
folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) {
|
||||||
|
folderUidRoute.Get("/", wrap(GetFolderByUid))
|
||||||
|
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
|
||||||
|
folderUidRoute.Delete("/", wrap(DeleteFolder))
|
||||||
|
|
||||||
|
folderUidRoute.Group("/permissions", func(folderPermissionRoute RouteRegister) {
|
||||||
|
folderPermissionRoute.Get("/", wrap(GetFolderPermissionList))
|
||||||
|
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
|
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
|
||||||
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
|
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
|
||||||
@ -266,9 +284,9 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
|
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
|
||||||
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||||
|
|
||||||
dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
|
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute RouteRegister) {
|
||||||
aclRoute.Get("/", wrap(GetDashboardAclList))
|
dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList))
|
||||||
aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
|
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardPermissions))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -137,6 +137,7 @@ func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dash
|
|||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
return nil, ApiError(404, "Dashboard not found", err)
|
return nil, ApiError(404, "Dashboard not found", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.Result, nil
|
return query.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,8 +167,10 @@ func DeleteDashboard(c *middleware.Context) Response {
|
|||||||
return ApiError(500, "Failed to delete dashboard", err)
|
return ApiError(500, "Failed to delete dashboard", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp = map[string]interface{}{"title": dash.Title}
|
return Json(200, util.DynMap{
|
||||||
return Json(200, resp)
|
"title": dash.Title,
|
||||||
|
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDashboardByUid(c *middleware.Context) Response {
|
func DeleteDashboardByUid(c *middleware.Context) Response {
|
||||||
@ -186,8 +189,10 @@ func DeleteDashboardByUid(c *middleware.Context) Response {
|
|||||||
return ApiError(500, "Failed to delete dashboard", err)
|
return ApiError(500, "Failed to delete dashboard", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp = map[string]interface{}{"title": dash.Title}
|
return Json(200, util.DynMap{
|
||||||
return Json(200, resp)
|
"title": dash.Title,
|
||||||
|
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetDashboardAclList(c *middleware.Context) Response {
|
func GetDashboardPermissionList(c *middleware.Context) Response {
|
||||||
dashId := c.ParamsInt64(":dashboardId")
|
dashId := c.ParamsInt64(":dashboardId")
|
||||||
|
|
||||||
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
|
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
|
||||||
@ -26,7 +26,7 @@ func GetDashboardAclList(c *middleware.Context) Response {
|
|||||||
|
|
||||||
acl, err := guardian.GetAcl()
|
acl, err := guardian.GetAcl()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ApiError(500, "Failed to get dashboard acl", err)
|
return ApiError(500, "Failed to get dashboard permissions", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, perm := range acl {
|
for _, perm := range acl {
|
||||||
@ -38,7 +38,7 @@ func GetDashboardAclList(c *middleware.Context) Response {
|
|||||||
return Json(200, acl)
|
return Json(200, acl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
|
func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
|
||||||
dashId := c.ParamsInt64(":dashboardId")
|
dashId := c.ParamsInt64(":dashboardId")
|
||||||
|
|
||||||
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
|
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
|
||||||
@ -82,5 +82,5 @@ func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCom
|
|||||||
return ApiError(500, "Failed to create permission", err)
|
return ApiError(500, "Failed to create permission", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiSuccess("Dashboard acl updated")
|
return ApiSuccess("Dashboard permissions updated")
|
||||||
}
|
}
|
@ -12,8 +12,8 @@ import (
|
|||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDashboardAclApiEndpoint(t *testing.T) {
|
func TestDashboardPermissionApiEndpoint(t *testing.T) {
|
||||||
Convey("Given a dashboard acl", t, func() {
|
Convey("Given a dashboard with permissions", t, func() {
|
||||||
mockResult := []*m.DashboardAclInfoDTO{
|
mockResult := []*m.DashboardAclInfoDTO{
|
||||||
{OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
|
{OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
|
||||||
{OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
|
{OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
|
||||||
@ -54,9 +54,9 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
|||||||
// 4. user is an org editor AND has no permissions for the dashboard
|
// 4. user is an org editor AND has no permissions for the dashboard
|
||||||
|
|
||||||
Convey("When user is org admin", func() {
|
Convey("When user is org admin", func() {
|
||||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardsId/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||||
Convey("Should be able to access ACL", func() {
|
Convey("Should be able to access ACL", func() {
|
||||||
sc.handlerFunc = GetDashboardAclList
|
sc.handlerFunc = GetDashboardPermissionList
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
So(sc.resp.Code, ShouldEqual, 200)
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
@ -69,9 +69,9 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||||
getDashboardNotFoundError = m.ErrDashboardNotFound
|
getDashboardNotFoundError = m.ErrDashboardNotFound
|
||||||
sc.handlerFunc = GetDashboardAclList
|
sc.handlerFunc = GetDashboardPermissionList
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
Convey("Should not be able to access ACL", func() {
|
Convey("Should not be able to access ACL", func() {
|
||||||
@ -86,7 +86,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, cmd, func(sc *scenarioContext) {
|
postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_ADMIN, cmd, func(sc *scenarioContext) {
|
||||||
getDashboardNotFoundError = m.ErrDashboardNotFound
|
getDashboardNotFoundError = m.ErrDashboardNotFound
|
||||||
CallPostAcl(sc)
|
CallPostAcl(sc)
|
||||||
So(sc.resp.Code, ShouldEqual, 404)
|
So(sc.resp.Code, ShouldEqual, 404)
|
||||||
@ -95,11 +95,11 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("When user is org editor and has admin permission in the ACL", func() {
|
Convey("When user is org editor and has admin permission in the ACL", func() {
|
||||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||||
|
|
||||||
Convey("Should be able to access ACL", func() {
|
Convey("Should be able to access ACL", func() {
|
||||||
sc.handlerFunc = GetDashboardAclList
|
sc.handlerFunc = GetDashboardPermissionList
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
So(sc.resp.Code, ShouldEqual, 200)
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
@ -113,7 +113,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
|
postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
|
||||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||||
|
|
||||||
CallPostAcl(sc)
|
CallPostAcl(sc)
|
||||||
@ -129,7 +129,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
|
postAclScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
|
||||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||||
|
|
||||||
CallPostAcl(sc)
|
CallPostAcl(sc)
|
||||||
@ -140,12 +140,12 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("When user is org viewer and has edit permission in the ACL", func() {
|
Convey("When user is org viewer and has edit permission in the ACL", func() {
|
||||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_VIEWER, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardId/permissions", m.ROLE_VIEWER, func(sc *scenarioContext) {
|
||||||
mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
|
||||||
|
|
||||||
// Getting the permissions is an Admin permission
|
// Getting the permissions is an Admin permission
|
||||||
Convey("Should not be able to get list of permissions from ACL", func() {
|
Convey("Should not be able to get list of permissions from ACL", func() {
|
||||||
sc.handlerFunc = GetDashboardAclList
|
sc.handlerFunc = GetDashboardPermissionList
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
So(sc.resp.Code, ShouldEqual, 403)
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
@ -154,10 +154,10 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("When user is org editor and not in the ACL", func() {
|
Convey("When user is org editor and not in the ACL", func() {
|
||||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:dashboardsId/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
|
||||||
Convey("Should not be able to access ACL", func() {
|
Convey("Should not be able to access ACL", func() {
|
||||||
sc.handlerFunc = GetDashboardAclList
|
sc.handlerFunc = GetDashboardPermissionList
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
So(sc.resp.Code, ShouldEqual, 403)
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
@ -204,7 +204,7 @@ func postAclScenario(desc string, url string, routePattern string, role m.RoleTy
|
|||||||
sc.context.OrgId = TestOrgID
|
sc.context.OrgId = TestOrgID
|
||||||
sc.context.OrgRole = role
|
sc.context.OrgRole = role
|
||||||
|
|
||||||
return UpdateDashboardAcl(c, cmd)
|
return UpdateDashboardPermissions(c, cmd)
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.m.Post(routePattern, sc.defaultHandler)
|
sc.m.Post(routePattern, sc.defaultHandler)
|
@ -746,8 +746,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||||
sc.handlerFunc = GetDashboard
|
CallGetDashboard(sc)
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
|
||||||
|
|
||||||
So(sc.resp.Code, ShouldEqual, 200)
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
|
||||||
@ -758,6 +757,11 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta
|
|||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CallGetDashboard(sc *scenarioContext) {
|
||||||
|
sc.handlerFunc = GetDashboard
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
func CallGetDashboardVersion(sc *scenarioContext) {
|
func CallGetDashboardVersion(sc *scenarioContext) {
|
||||||
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
|
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
|
||||||
query.Result = &m.DashboardVersion{}
|
query.Result = &m.DashboardVersion{}
|
||||||
|
25
pkg/api/dtos/folder.go
Normal file
25
pkg/api/dtos/folder.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package dtos
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Folder struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Uid string `json:"uid"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
HasAcl bool `json:"hasAcl"`
|
||||||
|
CanSave bool `json:"canSave"`
|
||||||
|
CanEdit bool `json:"canEdit"`
|
||||||
|
CanAdmin bool `json:"canAdmin"`
|
||||||
|
CreatedBy string `json:"createdBy"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
UpdatedBy string `json:"updatedBy"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FolderSearchHit struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Uid string `json:"uid"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
147
pkg/api/folder.go
Normal file
147
pkg/api/folder.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetFolders(c *middleware.Context) Response {
|
||||||
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
|
folders, err := s.GetFolders(c.QueryInt("limit"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]dtos.FolderSearchHit, 0)
|
||||||
|
|
||||||
|
for _, f := range folders {
|
||||||
|
result = append(result, dtos.FolderSearchHit{
|
||||||
|
Id: f.Id,
|
||||||
|
Uid: f.Uid,
|
||||||
|
Title: f.Title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFolderByUid(c *middleware.Context) Response {
|
||||||
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
|
folder, err := s.GetFolderByUid(c.Params(":uid"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
|
||||||
|
return Json(200, toFolderDto(g, folder))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFolderById(c *middleware.Context) Response {
|
||||||
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
|
folder, err := s.GetFolderById(c.ParamsInt64(":id"))
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
|
||||||
|
return Json(200, toFolderDto(g, folder))
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response {
|
||||||
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
|
err := s.CreateFolder(&cmd)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
|
||||||
|
return Json(200, toFolderDto(g, cmd.Result))
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response {
|
||||||
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
|
err := s.UpdateFolder(c.Params(":uid"), &cmd)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
|
||||||
|
return Json(200, toFolderDto(g, cmd.Result))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFolder(c *middleware.Context) Response {
|
||||||
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
|
f, err := s.DeleteFolder(c.Params(":uid"))
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, util.DynMap{
|
||||||
|
"title": f.Title,
|
||||||
|
"message": fmt.Sprintf("Folder %s deleted", f.Title),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder {
|
||||||
|
canEdit, _ := g.CanEdit()
|
||||||
|
canSave, _ := g.CanSave()
|
||||||
|
canAdmin, _ := g.CanAdmin()
|
||||||
|
|
||||||
|
// Finding creator and last updater of the folder
|
||||||
|
updater, creator := "Anonymous", "Anonymous"
|
||||||
|
if folder.CreatedBy > 0 {
|
||||||
|
creator = getUserLogin(folder.CreatedBy)
|
||||||
|
}
|
||||||
|
if folder.UpdatedBy > 0 {
|
||||||
|
updater = getUserLogin(folder.UpdatedBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dtos.Folder{
|
||||||
|
Id: folder.Id,
|
||||||
|
Uid: folder.Uid,
|
||||||
|
Title: folder.Title,
|
||||||
|
Url: folder.Url,
|
||||||
|
HasAcl: folder.HasAcl,
|
||||||
|
CanSave: canSave,
|
||||||
|
CanEdit: canEdit,
|
||||||
|
CanAdmin: canAdmin,
|
||||||
|
CreatedBy: creator,
|
||||||
|
Created: folder.Created,
|
||||||
|
UpdatedBy: updater,
|
||||||
|
Updated: folder.Updated,
|
||||||
|
Version: folder.Version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFolderError(err error) Response {
|
||||||
|
if err == m.ErrFolderTitleEmpty ||
|
||||||
|
err == m.ErrFolderSameNameExists ||
|
||||||
|
err == m.ErrFolderWithSameUIDExists ||
|
||||||
|
err == m.ErrDashboardTypeMismatch ||
|
||||||
|
err == m.ErrDashboardInvalidUid ||
|
||||||
|
err == m.ErrDashboardUidToLong {
|
||||||
|
return ApiError(400, err.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == m.ErrFolderAccessDenied {
|
||||||
|
return ApiError(403, "Access denied", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == m.ErrFolderNotFound {
|
||||||
|
return Json(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == m.ErrFolderVersionMismatch {
|
||||||
|
return Json(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiError(500, "Folder API error", err)
|
||||||
|
}
|
103
pkg/api/folder_permission.go
Normal file
103
pkg/api/folder_permission.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetFolderPermissionList(c *middleware.Context) Response {
|
||||||
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
|
folder, err := s.GetFolderByUid(c.Params(":uid"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
|
||||||
|
|
||||||
|
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
|
||||||
|
return toFolderError(m.ErrFolderAccessDenied)
|
||||||
|
}
|
||||||
|
|
||||||
|
acl, err := guardian.GetAcl()
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(500, "Failed to get folder permissions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range acl {
|
||||||
|
perm.FolderId = folder.Id
|
||||||
|
perm.DashboardId = 0
|
||||||
|
|
||||||
|
if perm.Slug != "" {
|
||||||
|
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
|
||||||
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
|
folder, err := s.GetFolderByUid(c.Params(":uid"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
guardian := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
|
||||||
|
canAdmin, err := guardian.CanAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canAdmin {
|
||||||
|
return toFolderError(m.ErrFolderAccessDenied)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := m.UpdateDashboardAclCommand{}
|
||||||
|
cmd.DashboardId = folder.Id
|
||||||
|
|
||||||
|
for _, item := range apiCmd.Items {
|
||||||
|
cmd.Items = append(cmd.Items, &m.DashboardAcl{
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
DashboardId: folder.Id,
|
||||||
|
UserId: item.UserId,
|
||||||
|
TeamId: item.TeamId,
|
||||||
|
Role: item.Role,
|
||||||
|
Permission: item.Permission,
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if okToUpdate, err := guardian.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(500, "Error while checking folder permissions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
|
if err == m.ErrDashboardAclInfoMissing {
|
||||||
|
err = m.ErrFolderAclInfoMissing
|
||||||
|
}
|
||||||
|
if err == m.ErrDashboardPermissionDashboardEmpty {
|
||||||
|
err = m.ErrFolderPermissionFolderEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == m.ErrFolderAclInfoMissing || err == m.ErrFolderPermissionFolderEmpty {
|
||||||
|
return ApiError(409, err.Error(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiError(500, "Failed to create permission", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("Folder permissions updated")
|
||||||
|
}
|
125
pkg/api/folder_permission_test.go
Normal file
125
pkg/api/folder_permission_test.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFolderPermissionApiEndpoint(t *testing.T) {
|
||||||
|
Convey("Folder permissions test", t, func() {
|
||||||
|
Convey("Given user has no admin permissions", func() {
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
|
||||||
|
|
||||||
|
mock := &fakeFolderService{
|
||||||
|
GetFolderByUidResult: &m.Folder{
|
||||||
|
Id: 1,
|
||||||
|
Uid: "uid",
|
||||||
|
Title: "Folder",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
origNewFolderService := dashboards.NewFolderService
|
||||||
|
mockFolderService(mock)
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
callGetFolderPermissions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := dtos.UpdateDashboardAclCommand{
|
||||||
|
Items: []dtos.DashboardAclUpdateItem{
|
||||||
|
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
|
||||||
|
callUpdateFolderPermissions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
Reset(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
dashboards.NewFolderService = origNewFolderService
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given user has admin permissions and permissions to update", func() {
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: true, CheckPermissionBeforeUpdateValue: true})
|
||||||
|
|
||||||
|
mock := &fakeFolderService{
|
||||||
|
GetFolderByUidResult: &m.Folder{
|
||||||
|
Id: 1,
|
||||||
|
Uid: "uid",
|
||||||
|
Title: "Folder",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
origNewFolderService := dashboards.NewFolderService
|
||||||
|
mockFolderService(mock)
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||||
|
callGetFolderPermissions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := dtos.UpdateDashboardAclCommand{
|
||||||
|
Items: []dtos.DashboardAclUpdateItem{
|
||||||
|
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
|
||||||
|
callUpdateFolderPermissions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
Reset(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
dashboards.NewFolderService = origNewFolderService
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func callGetFolderPermissions(sc *scenarioContext) {
|
||||||
|
sc.handlerFunc = GetFolderPermissionList
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func callUpdateFolderPermissions(sc *scenarioContext) {
|
||||||
|
bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFolderPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
sc := setupScenarioContext(url)
|
||||||
|
|
||||||
|
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.UserId = TestUserID
|
||||||
|
|
||||||
|
return UpdateFolderPermissions(c, cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
254
pkg/api/folder_test.go
Normal file
254
pkg/api/folder_test.go
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFoldersApiEndpoint(t *testing.T) {
|
||||||
|
Convey("Create/update folder response tests", t, func() {
|
||||||
|
Convey("Given a correct request for creating a folder", func() {
|
||||||
|
cmd := m.CreateFolderCommand{
|
||||||
|
Uid: "uid",
|
||||||
|
Title: "Folder",
|
||||||
|
}
|
||||||
|
|
||||||
|
mock := &fakeFolderService{
|
||||||
|
CreateFolderResult: &m.Folder{Id: 1, Uid: "uid", Title: "Folder"},
|
||||||
|
}
|
||||||
|
|
||||||
|
createFolderScenario("When calling POST on", "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) {
|
||||||
|
callCreateFolder(sc)
|
||||||
|
|
||||||
|
Convey("It should return correct response data", func() {
|
||||||
|
folder := dtos.Folder{}
|
||||||
|
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(folder.Id, ShouldEqual, 1)
|
||||||
|
So(folder.Uid, ShouldEqual, "uid")
|
||||||
|
So(folder.Title, ShouldEqual, "Folder")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given incorrect requests for creating a folder", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
Error error
|
||||||
|
ExpectedStatusCode int
|
||||||
|
}{
|
||||||
|
{Error: m.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrFolderTitleEmpty, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrFolderSameNameExists, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrFolderAccessDenied, ExpectedStatusCode: 403},
|
||||||
|
{Error: m.ErrFolderNotFound, ExpectedStatusCode: 404},
|
||||||
|
{Error: m.ErrFolderVersionMismatch, ExpectedStatusCode: 412},
|
||||||
|
{Error: m.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := m.CreateFolderCommand{
|
||||||
|
Uid: "uid",
|
||||||
|
Title: "Folder",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
mock := &fakeFolderService{
|
||||||
|
CreateFolderError: tc.Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
createFolderScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()), "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) {
|
||||||
|
callCreateFolder(sc)
|
||||||
|
if sc.resp.Code != tc.ExpectedStatusCode {
|
||||||
|
t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given a correct request for updating a folder", func() {
|
||||||
|
cmd := m.UpdateFolderCommand{
|
||||||
|
Title: "Folder upd",
|
||||||
|
}
|
||||||
|
|
||||||
|
mock := &fakeFolderService{
|
||||||
|
UpdateFolderResult: &m.Folder{Id: 1, Uid: "uid", Title: "Folder upd"},
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) {
|
||||||
|
callUpdateFolder(sc)
|
||||||
|
|
||||||
|
Convey("It should return correct response data", func() {
|
||||||
|
folder := dtos.Folder{}
|
||||||
|
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(folder.Id, ShouldEqual, 1)
|
||||||
|
So(folder.Uid, ShouldEqual, "uid")
|
||||||
|
So(folder.Title, ShouldEqual, "Folder upd")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given incorrect requests for updating a folder", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
Error error
|
||||||
|
ExpectedStatusCode int
|
||||||
|
}{
|
||||||
|
{Error: m.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrFolderTitleEmpty, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrFolderSameNameExists, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
|
||||||
|
{Error: m.ErrFolderAccessDenied, ExpectedStatusCode: 403},
|
||||||
|
{Error: m.ErrFolderNotFound, ExpectedStatusCode: 404},
|
||||||
|
{Error: m.ErrFolderVersionMismatch, ExpectedStatusCode: 412},
|
||||||
|
{Error: m.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := m.UpdateFolderCommand{
|
||||||
|
Title: "Folder upd",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
mock := &fakeFolderService{
|
||||||
|
UpdateFolderError: tc.Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFolderScenario(fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()), "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) {
|
||||||
|
callUpdateFolder(sc)
|
||||||
|
if sc.resp.Code != tc.ExpectedStatusCode {
|
||||||
|
t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func callGetFolderByUid(sc *scenarioContext) {
|
||||||
|
sc.handlerFunc = GetFolderByUid
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func callDeleteFolder(sc *scenarioContext) {
|
||||||
|
sc.handlerFunc = DeleteFolder
|
||||||
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func callCreateFolder(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd m.CreateFolderCommand, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
sc := setupScenarioContext(url)
|
||||||
|
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
|
||||||
|
|
||||||
|
return CreateFolder(c, cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
origNewFolderService := dashboards.NewFolderService
|
||||||
|
mockFolderService(mock)
|
||||||
|
|
||||||
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
dashboards.NewFolderService = origNewFolderService
|
||||||
|
}()
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func callUpdateFolder(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd m.UpdateFolderCommand, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
sc := setupScenarioContext(url)
|
||||||
|
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
|
||||||
|
|
||||||
|
return UpdateFolder(c, cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
origNewFolderService := dashboards.NewFolderService
|
||||||
|
mockFolderService(mock)
|
||||||
|
|
||||||
|
sc.m.Put(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
dashboards.NewFolderService = origNewFolderService
|
||||||
|
}()
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeFolderService struct {
|
||||||
|
GetFoldersResult []*models.Folder
|
||||||
|
GetFoldersError error
|
||||||
|
GetFolderByUidResult *models.Folder
|
||||||
|
GetFolderByUidError error
|
||||||
|
GetFolderByIdResult *models.Folder
|
||||||
|
GetFolderByIdError error
|
||||||
|
CreateFolderResult *models.Folder
|
||||||
|
CreateFolderError error
|
||||||
|
UpdateFolderResult *models.Folder
|
||||||
|
UpdateFolderError error
|
||||||
|
DeleteFolderResult *models.Folder
|
||||||
|
DeleteFolderError error
|
||||||
|
DeletedFolderUids []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeFolderService) GetFolders(limit int) ([]*models.Folder, error) {
|
||||||
|
return s.GetFoldersResult, s.GetFoldersError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeFolderService) GetFolderById(id int64) (*models.Folder, error) {
|
||||||
|
return s.GetFolderByIdResult, s.GetFolderByIdError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeFolderService) GetFolderByUid(uid string) (*models.Folder, error) {
|
||||||
|
return s.GetFolderByUidResult, s.GetFolderByUidError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeFolderService) CreateFolder(cmd *models.CreateFolderCommand) error {
|
||||||
|
cmd.Result = s.CreateFolderResult
|
||||||
|
return s.CreateFolderError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error {
|
||||||
|
cmd.Result = s.UpdateFolderResult
|
||||||
|
return s.UpdateFolderError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeFolderService) DeleteFolder(uid string) (*models.Folder, error) {
|
||||||
|
s.DeletedFolderUids = append(s.DeletedFolderUids, uid)
|
||||||
|
return s.DeleteFolderResult, s.DeleteFolderError
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockFolderService(mock *fakeFolderService) {
|
||||||
|
dashboards.NewFolderService = func(orgId int64, user *models.SignedInUser) dashboards.FolderService {
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,8 @@ func (p PermissionType) String() string {
|
|||||||
var (
|
var (
|
||||||
ErrDashboardAclInfoMissing = errors.New("User id and team id cannot both be empty for a dashboard permission.")
|
ErrDashboardAclInfoMissing = errors.New("User id and team id cannot both be empty for a dashboard permission.")
|
||||||
ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.")
|
ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.")
|
||||||
|
ErrFolderAclInfoMissing = errors.New("User id and team id cannot both be empty for a folder permission.")
|
||||||
|
ErrFolderPermissionFolderEmpty = errors.New("Folder Id must be greater than zero for a folder permission.")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dashboard ACL model
|
// Dashboard ACL model
|
||||||
@ -45,7 +47,8 @@ type DashboardAcl struct {
|
|||||||
|
|
||||||
type DashboardAclInfoDTO struct {
|
type DashboardAclInfoDTO struct {
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
DashboardId int64 `json:"dashboardId"`
|
DashboardId int64 `json:"dashboardId,omitempty"`
|
||||||
|
FolderId int64 `json:"folderId,omitempty"`
|
||||||
|
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
// Typed errors
|
// Typed errors
|
||||||
var (
|
var (
|
||||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||||
ErrFolderNotFound = 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")
|
||||||
@ -112,9 +112,9 @@ func NewDashboard(title string) *Dashboard {
|
|||||||
// NewDashboardFolder creates a new dashboard folder
|
// NewDashboardFolder creates a new dashboard folder
|
||||||
func NewDashboardFolder(title string) *Dashboard {
|
func NewDashboardFolder(title string) *Dashboard {
|
||||||
folder := NewDashboard(title)
|
folder := NewDashboard(title)
|
||||||
|
folder.IsFolder = true
|
||||||
folder.Data.Set("schemaVersion", 16)
|
folder.Data.Set("schemaVersion", 16)
|
||||||
folder.Data.Set("editable", true)
|
folder.Data.Set("version", 0)
|
||||||
folder.Data.Set("hideControls", true)
|
|
||||||
folder.IsFolder = true
|
folder.IsFolder = true
|
||||||
return folder
|
return folder
|
||||||
}
|
}
|
||||||
@ -166,10 +166,6 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
|||||||
userId = -1
|
userId = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if dash.Data.Get("version").MustInt(0) == 0 {
|
|
||||||
dash.CreatedBy = userId
|
|
||||||
}
|
|
||||||
|
|
||||||
dash.UpdatedBy = userId
|
dash.UpdatedBy = userId
|
||||||
dash.OrgId = cmd.OrgId
|
dash.OrgId = cmd.OrgId
|
||||||
dash.PluginId = cmd.PluginId
|
dash.PluginId = cmd.PluginId
|
||||||
|
91
pkg/models/folders.go
Normal file
91
pkg/models/folders.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Typed errors
|
||||||
|
var (
|
||||||
|
ErrFolderNotFound = errors.New("Folder not found")
|
||||||
|
ErrFolderVersionMismatch = errors.New("The folder has been changed by someone else")
|
||||||
|
ErrFolderTitleEmpty = errors.New("Folder title cannot be empty")
|
||||||
|
ErrFolderWithSameUIDExists = errors.New("A folder/dashboard with the same uid already exists")
|
||||||
|
ErrFolderSameNameExists = errors.New("A folder or dashboard in the general folder with the same name already exists")
|
||||||
|
ErrFolderFailedGenerateUniqueUid = errors.New("Failed to generate unique folder id")
|
||||||
|
ErrFolderAccessDenied = errors.New("Access denied to folder")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Folder struct {
|
||||||
|
Id int64
|
||||||
|
Uid string
|
||||||
|
Title string
|
||||||
|
Url string
|
||||||
|
Version int
|
||||||
|
|
||||||
|
Created time.Time
|
||||||
|
Updated time.Time
|
||||||
|
|
||||||
|
UpdatedBy int64
|
||||||
|
CreatedBy int64
|
||||||
|
HasAcl bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboardModel turns the command into the savable model
|
||||||
|
func (cmd *CreateFolderCommand) GetDashboardModel(orgId int64, userId int64) *Dashboard {
|
||||||
|
dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title))
|
||||||
|
dashFolder.OrgId = orgId
|
||||||
|
dashFolder.SetUid(strings.TrimSpace(cmd.Uid))
|
||||||
|
|
||||||
|
if userId == 0 {
|
||||||
|
userId = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
dashFolder.CreatedBy = userId
|
||||||
|
dashFolder.UpdatedBy = userId
|
||||||
|
dashFolder.UpdateSlug()
|
||||||
|
|
||||||
|
return dashFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDashboardModel updates an existing model from command into model for update
|
||||||
|
func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard, orgId int64, userId int64) {
|
||||||
|
dashFolder.OrgId = orgId
|
||||||
|
dashFolder.Title = strings.TrimSpace(cmd.Title)
|
||||||
|
dashFolder.Data.Set("title", dashFolder.Title)
|
||||||
|
|
||||||
|
if cmd.Uid != "" {
|
||||||
|
dashFolder.SetUid(cmd.Uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
dashFolder.SetVersion(cmd.Version)
|
||||||
|
dashFolder.IsFolder = true
|
||||||
|
|
||||||
|
if userId == 0 {
|
||||||
|
userId = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
dashFolder.UpdatedBy = userId
|
||||||
|
dashFolder.UpdateSlug()
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// COMMANDS
|
||||||
|
//
|
||||||
|
|
||||||
|
type CreateFolderCommand struct {
|
||||||
|
Uid string `json:"uid"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
|
||||||
|
Result *Folder
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateFolderCommand struct {
|
||||||
|
Uid string `json:"uid"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
Overwrite bool `json:"overwrite"`
|
||||||
|
|
||||||
|
Result *Folder
|
||||||
|
}
|
@ -41,7 +41,10 @@ type SaveDashboardDTO struct {
|
|||||||
Dashboard *models.Dashboard
|
Dashboard *models.Dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
type dashboardServiceImpl struct{}
|
type dashboardServiceImpl struct {
|
||||||
|
orgId int64
|
||||||
|
user *models.SignedInUser
|
||||||
|
}
|
||||||
|
|
||||||
func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||||
cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
|
cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
|
||||||
@ -53,7 +56,7 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
|
|||||||
return cmd.Result, nil
|
return cmd.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) {
|
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) {
|
||||||
dash := dto.Dashboard
|
dash := dto.Dashboard
|
||||||
|
|
||||||
dash.Title = strings.TrimSpace(dash.Title)
|
dash.Title = strings.TrimSpace(dash.Title)
|
||||||
@ -78,13 +81,15 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO)
|
|||||||
return nil, models.ErrDashboardUidToLong
|
return nil, models.ErrDashboardUidToLong
|
||||||
}
|
}
|
||||||
|
|
||||||
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
|
if validateAlerts {
|
||||||
OrgId: dto.OrgId,
|
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
|
||||||
Dashboard: dash,
|
OrgId: dto.OrgId,
|
||||||
}
|
Dashboard: dash,
|
||||||
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||||
return nil, models.ErrDashboardContainsInvalidAlertData
|
return nil, models.ErrDashboardContainsInvalidAlertData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
|
validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
|
||||||
@ -142,7 +147,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
|
|||||||
UserId: 0,
|
UserId: 0,
|
||||||
OrgRole: models.ROLE_ADMIN,
|
OrgRole: models.ROLE_ADMIN,
|
||||||
}
|
}
|
||||||
cmd, err := dr.buildSaveDashboardCommand(dto)
|
cmd, err := dr.buildSaveDashboardCommand(dto, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -172,7 +177,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
|
|||||||
UserId: 0,
|
UserId: 0,
|
||||||
OrgRole: models.ROLE_ADMIN,
|
OrgRole: models.ROLE_ADMIN,
|
||||||
}
|
}
|
||||||
cmd, err := dr.buildSaveDashboardCommand(dto)
|
cmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -191,7 +196,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||||
cmd, err := dr.buildSaveDashboardCommand(dto)
|
cmd, err := dr.buildSaveDashboardCommand(dto, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ func TestDashboardService(t *testing.T) {
|
|||||||
service := dashboardServiceImpl{}
|
service := dashboardServiceImpl{}
|
||||||
|
|
||||||
origNewDashboardGuardian := guardian.New
|
origNewDashboardGuardian := guardian.New
|
||||||
mockDashboardGuardian(&fakeDashboardGuardian{canSave: true})
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
||||||
|
|
||||||
Convey("Save dashboard validation", func() {
|
Convey("Save dashboard validation", func() {
|
||||||
dto := &SaveDashboardDTO{}
|
dto := &SaveDashboardDTO{}
|
||||||
@ -72,7 +72,7 @@ func TestDashboardService(t *testing.T) {
|
|||||||
dto.Dashboard.SetUid(tc.Uid)
|
dto.Dashboard.SetUid(tc.Uid)
|
||||||
dto.User = &models.SignedInUser{}
|
dto.User = &models.SignedInUser{}
|
||||||
|
|
||||||
_, err := service.buildSaveDashboardCommand(dto)
|
_, err := service.buildSaveDashboardCommand(dto, true)
|
||||||
So(err, ShouldEqual, tc.Error)
|
So(err, ShouldEqual, tc.Error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -93,52 +93,3 @@ func TestDashboardService(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func mockDashboardGuardian(mock *fakeDashboardGuardian) {
|
|
||||||
guardian.New = func(dashId int64, orgId int64, user *models.SignedInUser) guardian.DashboardGuardian {
|
|
||||||
mock.orgId = orgId
|
|
||||||
mock.dashId = dashId
|
|
||||||
mock.user = user
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeDashboardGuardian struct {
|
|
||||||
dashId int64
|
|
||||||
orgId int64
|
|
||||||
user *models.SignedInUser
|
|
||||||
canSave bool
|
|
||||||
canEdit bool
|
|
||||||
canView bool
|
|
||||||
canAdmin bool
|
|
||||||
hasPermission bool
|
|
||||||
checkPermissionBeforeUpdate bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *fakeDashboardGuardian) CanSave() (bool, error) {
|
|
||||||
return g.canSave, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *fakeDashboardGuardian) CanEdit() (bool, error) {
|
|
||||||
return g.canEdit, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *fakeDashboardGuardian) CanView() (bool, error) {
|
|
||||||
return g.canView, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *fakeDashboardGuardian) CanAdmin() (bool, error) {
|
|
||||||
return g.canAdmin, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *fakeDashboardGuardian) HasPermission(permission models.PermissionType) (bool, error) {
|
|
||||||
return g.hasPermission, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *fakeDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
|
|
||||||
return g.checkPermissionBeforeUpdate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *fakeDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
245
pkg/services/dashboards/folder_service.go
Normal file
245
pkg/services/dashboards/folder_service.go
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
package dashboards
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
|
"github.com/grafana/grafana/pkg/services/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FolderService service for operating on folders
|
||||||
|
type FolderService interface {
|
||||||
|
GetFolders(limit int) ([]*models.Folder, error)
|
||||||
|
GetFolderById(id int64) (*models.Folder, error)
|
||||||
|
GetFolderByUid(uid string) (*models.Folder, error)
|
||||||
|
CreateFolder(cmd *models.CreateFolderCommand) error
|
||||||
|
UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
|
||||||
|
DeleteFolder(uid string) (*models.Folder, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFolderService factory for creating a new folder service
|
||||||
|
var NewFolderService = func(orgId int64, user *models.SignedInUser) FolderService {
|
||||||
|
return &dashboardServiceImpl{
|
||||||
|
orgId: orgId,
|
||||||
|
user: user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *dashboardServiceImpl) GetFolders(limit int) ([]*models.Folder, error) {
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
searchQuery := search.Query{
|
||||||
|
SignedInUser: dr.user,
|
||||||
|
DashboardIds: make([]int64, 0),
|
||||||
|
FolderIds: make([]int64, 0),
|
||||||
|
Limit: limit,
|
||||||
|
OrgId: dr.orgId,
|
||||||
|
Type: "dash-folder",
|
||||||
|
Permission: models.PERMISSION_VIEW,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&searchQuery); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
folders := make([]*models.Folder, 0)
|
||||||
|
|
||||||
|
for _, hit := range searchQuery.Result {
|
||||||
|
folders = append(folders, &models.Folder{
|
||||||
|
Id: hit.Id,
|
||||||
|
Uid: hit.Uid,
|
||||||
|
Title: hit.Title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *dashboardServiceImpl) GetFolderById(id int64) (*models.Folder, error) {
|
||||||
|
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id}
|
||||||
|
dashFolder, err := getFolder(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := guardian.New(dashFolder.Id, dr.orgId, dr.user)
|
||||||
|
if canView, err := g.CanView(); err != nil || !canView {
|
||||||
|
if err != nil {
|
||||||
|
return nil, toFolderError(err)
|
||||||
|
}
|
||||||
|
return nil, models.ErrFolderAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashToFolder(dashFolder), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *dashboardServiceImpl) GetFolderByUid(uid string) (*models.Folder, error) {
|
||||||
|
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
|
||||||
|
dashFolder, err := getFolder(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := guardian.New(dashFolder.Id, dr.orgId, dr.user)
|
||||||
|
if canView, err := g.CanView(); err != nil || !canView {
|
||||||
|
if err != nil {
|
||||||
|
return nil, toFolderError(err)
|
||||||
|
}
|
||||||
|
return nil, models.ErrFolderAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashToFolder(dashFolder), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) error {
|
||||||
|
dashFolder := cmd.GetDashboardModel(dr.orgId, dr.user.UserId)
|
||||||
|
|
||||||
|
dto := &SaveDashboardDTO{
|
||||||
|
Dashboard: dashFolder,
|
||||||
|
OrgId: dr.orgId,
|
||||||
|
User: dr.user,
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bus.Dispatch(saveDashboardCmd)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id}
|
||||||
|
dashFolder, err = getFolder(query)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Result = dashToFolder(dashFolder)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error {
|
||||||
|
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: existingUid}
|
||||||
|
dashFolder, err := getFolder(query)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.UpdateDashboardModel(dashFolder, dr.orgId, dr.user.UserId)
|
||||||
|
|
||||||
|
dto := &SaveDashboardDTO{
|
||||||
|
Dashboard: dashFolder,
|
||||||
|
OrgId: dr.orgId,
|
||||||
|
User: dr.user,
|
||||||
|
Overwrite: cmd.Overwrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bus.Dispatch(saveDashboardCmd)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id}
|
||||||
|
dashFolder, err = getFolder(query)
|
||||||
|
if err != nil {
|
||||||
|
return toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Result = dashToFolder(dashFolder)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *dashboardServiceImpl) DeleteFolder(uid string) (*models.Folder, error) {
|
||||||
|
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
|
||||||
|
dashFolder, err := getFolder(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
guardian := guardian.New(dashFolder.Id, dr.orgId, dr.user)
|
||||||
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||||
|
if err != nil {
|
||||||
|
return nil, toFolderError(err)
|
||||||
|
}
|
||||||
|
return nil, models.ErrFolderAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCmd := models.DeleteDashboardCommand{OrgId: dr.orgId, Id: dashFolder.Id}
|
||||||
|
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||||
|
return nil, toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashToFolder(dashFolder), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFolder(query models.GetDashboardQuery) (*models.Dashboard, error) {
|
||||||
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
return nil, toFolderError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !query.Result.IsFolder {
|
||||||
|
return nil, models.ErrFolderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dashToFolder(dash *models.Dashboard) *models.Folder {
|
||||||
|
return &models.Folder{
|
||||||
|
Id: dash.Id,
|
||||||
|
Uid: dash.Uid,
|
||||||
|
Title: dash.Title,
|
||||||
|
HasAcl: dash.HasAcl,
|
||||||
|
Url: dash.GetUrl(),
|
||||||
|
Version: dash.Version,
|
||||||
|
Created: dash.Created,
|
||||||
|
CreatedBy: dash.CreatedBy,
|
||||||
|
Updated: dash.Updated,
|
||||||
|
UpdatedBy: dash.UpdatedBy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFolderError(err error) error {
|
||||||
|
if err == models.ErrDashboardTitleEmpty {
|
||||||
|
return models.ErrFolderTitleEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == models.ErrDashboardUpdateAccessDenied {
|
||||||
|
return models.ErrFolderAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == models.ErrDashboardWithSameNameInFolderExists {
|
||||||
|
return models.ErrFolderSameNameExists
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == models.ErrDashboardWithSameUIDExists {
|
||||||
|
return models.ErrFolderWithSameUIDExists
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == models.ErrDashboardVersionMismatch {
|
||||||
|
return models.ErrFolderVersionMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == models.ErrDashboardNotFound {
|
||||||
|
return models.ErrFolderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == models.ErrDashboardFailedGenerateUniqueUid {
|
||||||
|
err = models.ErrFolderFailedGenerateUniqueUid
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
191
pkg/services/dashboards/folder_service_test.go
Normal file
191
pkg/services/dashboards/folder_service_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package dashboards
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFolderService(t *testing.T) {
|
||||||
|
Convey("Folder service tests", t, func() {
|
||||||
|
service := dashboardServiceImpl{
|
||||||
|
orgId: 1,
|
||||||
|
user: &models.SignedInUser{UserId: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("Given user has no permissions", func() {
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
|
||||||
|
query.Result = models.NewDashboardFolder("Folder")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||||
|
return models.ErrDashboardUpdateAccessDenied
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When get folder by id should return access denied error", func() {
|
||||||
|
_, err := service.GetFolderById(1)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When get folder by uid should return access denied error", func() {
|
||||||
|
_, err := service.GetFolderByUid("uid")
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When creating folder should return access denied error", func() {
|
||||||
|
err := service.CreateFolder(&models.CreateFolderCommand{
|
||||||
|
Title: "Folder",
|
||||||
|
})
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When updating folder should return access denied error", func() {
|
||||||
|
err := service.UpdateFolder("uid", &models.UpdateFolderCommand{
|
||||||
|
Uid: "uid",
|
||||||
|
Title: "Folder",
|
||||||
|
})
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When deleting folder by uid should return access denied error", func() {
|
||||||
|
_, err := service.DeleteFolder("uid")
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||||
|
})
|
||||||
|
|
||||||
|
Reset(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given user has permission to save", func() {
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
||||||
|
|
||||||
|
dash := models.NewDashboardFolder("Folder")
|
||||||
|
dash.Id = 1
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
|
||||||
|
query.Result = dash
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *models.SaveDashboardCommand) error {
|
||||||
|
cmd.Result = dash
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When creating folder should not return access denied error", func() {
|
||||||
|
err := service.CreateFolder(&models.CreateFolderCommand{
|
||||||
|
Title: "Folder",
|
||||||
|
})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When updating folder should not return access denied error", func() {
|
||||||
|
err := service.UpdateFolder("uid", &models.UpdateFolderCommand{
|
||||||
|
Uid: "uid",
|
||||||
|
Title: "Folder",
|
||||||
|
})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When deleting folder by uid should not return access denied error", func() {
|
||||||
|
_, err := service.DeleteFolder("uid")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Reset(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given user has permission to view", func() {
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true})
|
||||||
|
|
||||||
|
dashFolder := models.NewDashboardFolder("Folder")
|
||||||
|
dashFolder.Id = 1
|
||||||
|
dashFolder.Uid = "uid-abc"
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
|
||||||
|
query.Result = dashFolder
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When get folder by id should return folder", func() {
|
||||||
|
f, _ := service.GetFolderById(1)
|
||||||
|
So(f.Id, ShouldEqual, dashFolder.Id)
|
||||||
|
So(f.Uid, ShouldEqual, dashFolder.Uid)
|
||||||
|
So(f.Title, ShouldEqual, dashFolder.Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When get folder by uid should return folder", func() {
|
||||||
|
f, _ := service.GetFolderByUid("uid")
|
||||||
|
So(f.Id, ShouldEqual, dashFolder.Id)
|
||||||
|
So(f.Uid, ShouldEqual, dashFolder.Uid)
|
||||||
|
So(f.Title, ShouldEqual, dashFolder.Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
Reset(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should map errors correct", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
ActualError error
|
||||||
|
ExpectedError error
|
||||||
|
}{
|
||||||
|
{ActualError: models.ErrDashboardTitleEmpty, ExpectedError: models.ErrFolderTitleEmpty},
|
||||||
|
{ActualError: models.ErrDashboardUpdateAccessDenied, ExpectedError: models.ErrFolderAccessDenied},
|
||||||
|
{ActualError: models.ErrDashboardWithSameNameInFolderExists, ExpectedError: models.ErrFolderSameNameExists},
|
||||||
|
{ActualError: models.ErrDashboardWithSameUIDExists, ExpectedError: models.ErrFolderWithSameUIDExists},
|
||||||
|
{ActualError: models.ErrDashboardVersionMismatch, ExpectedError: models.ErrFolderVersionMismatch},
|
||||||
|
{ActualError: models.ErrDashboardNotFound, ExpectedError: models.ErrFolderNotFound},
|
||||||
|
{ActualError: models.ErrDashboardFailedGenerateUniqueUid, ExpectedError: models.ErrFolderFailedGenerateUniqueUid},
|
||||||
|
{ActualError: models.ErrDashboardInvalidUid, ExpectedError: models.ErrDashboardInvalidUid},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
actualError := toFolderError(tc.ActualError)
|
||||||
|
if actualError != tc.ExpectedError {
|
||||||
|
t.Errorf("For error '%s' expected error '%s', actual '%s'", tc.ActualError, tc.ExpectedError, actualError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -158,3 +158,52 @@ func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) {
|
|||||||
g.groups = query.Result
|
g.groups = query.Result
|
||||||
return query.Result, err
|
return query.Result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FakeDashboardGuardian struct {
|
||||||
|
DashId int64
|
||||||
|
OrgId int64
|
||||||
|
User *m.SignedInUser
|
||||||
|
CanSaveValue bool
|
||||||
|
CanEditValue bool
|
||||||
|
CanViewValue bool
|
||||||
|
CanAdminValue bool
|
||||||
|
HasPermissionValue bool
|
||||||
|
CheckPermissionBeforeUpdateValue bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FakeDashboardGuardian) CanSave() (bool, error) {
|
||||||
|
return g.CanSaveValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FakeDashboardGuardian) CanEdit() (bool, error) {
|
||||||
|
return g.CanEditValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FakeDashboardGuardian) CanView() (bool, error) {
|
||||||
|
return g.CanViewValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FakeDashboardGuardian) CanAdmin() (bool, error) {
|
||||||
|
return g.CanAdminValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FakeDashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
|
||||||
|
return g.HasPermissionValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
|
||||||
|
return g.CheckPermissionBeforeUpdateValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FakeDashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MockDashboardGuardian(mock *FakeDashboardGuardian) {
|
||||||
|
New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardian {
|
||||||
|
mock.OrgId = orgId
|
||||||
|
mock.DashId = dashId
|
||||||
|
mock.User = user
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -37,6 +37,12 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
|||||||
func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
|
func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
|
||||||
dash := cmd.GetDashboardModel()
|
dash := cmd.GetDashboardModel()
|
||||||
|
|
||||||
|
userId := cmd.UserId
|
||||||
|
|
||||||
|
if userId == 0 {
|
||||||
|
userId = -1
|
||||||
|
}
|
||||||
|
|
||||||
if dash.Id > 0 {
|
if dash.Id > 0 {
|
||||||
var existing m.Dashboard
|
var existing m.Dashboard
|
||||||
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
|
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
|
||||||
@ -76,17 +82,23 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
|
|||||||
|
|
||||||
if dash.Id == 0 {
|
if dash.Id == 0 {
|
||||||
dash.SetVersion(1)
|
dash.SetVersion(1)
|
||||||
|
dash.Created = time.Now()
|
||||||
|
dash.CreatedBy = userId
|
||||||
|
dash.Updated = time.Now()
|
||||||
|
dash.UpdatedBy = userId
|
||||||
metrics.M_Api_Dashboard_Insert.Inc()
|
metrics.M_Api_Dashboard_Insert.Inc()
|
||||||
affectedRows, err = sess.Insert(dash)
|
affectedRows, err = sess.Insert(dash)
|
||||||
} else {
|
} else {
|
||||||
v := dash.Version
|
dash.SetVersion(dash.Version + 1)
|
||||||
v++
|
|
||||||
dash.SetVersion(v)
|
|
||||||
|
|
||||||
if !cmd.UpdatedAt.IsZero() {
|
if !cmd.UpdatedAt.IsZero() {
|
||||||
dash.Updated = cmd.UpdatedAt
|
dash.Updated = cmd.UpdatedAt
|
||||||
|
} else {
|
||||||
|
dash.Updated = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dash.UpdatedBy = userId
|
||||||
|
|
||||||
affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash)
|
affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,7 +526,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !folderExists {
|
if !folderExists {
|
||||||
return m.ErrFolderNotFound
|
return m.ErrDashboardFolderNotFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,9 +142,9 @@ func TestIntegratedDashboardService(t *testing.T) {
|
|||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||||
|
|
||||||
So(sc.dashboardGuardianMock.dashId, ShouldEqual, 0)
|
So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
|
||||||
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
|
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||||
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
|
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -165,9 +165,9 @@ func TestIntegratedDashboardService(t *testing.T) {
|
|||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||||
|
|
||||||
So(sc.dashboardGuardianMock.dashId, ShouldEqual, otherSavedFolder.Id)
|
So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
|
||||||
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
|
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||||
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
|
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -189,9 +189,9 @@ func TestIntegratedDashboardService(t *testing.T) {
|
|||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||||
|
|
||||||
So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInGeneralFolder.Id)
|
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedDashInGeneralFolder.Id)
|
||||||
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
|
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||||
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
|
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -213,9 +213,9 @@ func TestIntegratedDashboardService(t *testing.T) {
|
|||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||||
|
|
||||||
So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInFolder.Id)
|
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedDashInFolder.Id)
|
||||||
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
|
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||||
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
|
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -363,7 +363,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
|||||||
|
|
||||||
Convey("It should result in folder not found error", func() {
|
Convey("It should result in folder not found error", func() {
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
So(err, ShouldEqual, models.ErrFolderNotFound)
|
So(err, ShouldEqual, models.ErrDashboardFolderNotFound)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -785,68 +785,16 @@ func TestIntegratedDashboardService(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func mockDashboardGuardian(mock *mockDashboardGuarder) {
|
|
||||||
guardian.New = func(dashId int64, orgId int64, user *models.SignedInUser) guardian.DashboardGuardian {
|
|
||||||
mock.orgId = orgId
|
|
||||||
mock.dashId = dashId
|
|
||||||
mock.user = user
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockDashboardGuarder struct {
|
|
||||||
dashId int64
|
|
||||||
orgId int64
|
|
||||||
user *models.SignedInUser
|
|
||||||
canSave bool
|
|
||||||
canSaveCallCounter int
|
|
||||||
canEdit bool
|
|
||||||
canView bool
|
|
||||||
canAdmin bool
|
|
||||||
hasPermission bool
|
|
||||||
checkPermissionBeforeRemove bool
|
|
||||||
checkPermissionBeforeUpdate bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *mockDashboardGuarder) CanSave() (bool, error) {
|
|
||||||
g.canSaveCallCounter++
|
|
||||||
return g.canSave, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *mockDashboardGuarder) CanEdit() (bool, error) {
|
|
||||||
return g.canEdit, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *mockDashboardGuarder) CanView() (bool, error) {
|
|
||||||
return g.canView, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *mockDashboardGuarder) CanAdmin() (bool, error) {
|
|
||||||
return g.canAdmin, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *mockDashboardGuarder) HasPermission(permission models.PermissionType) (bool, error) {
|
|
||||||
return g.hasPermission, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *mockDashboardGuarder) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
|
|
||||||
return g.checkPermissionBeforeUpdate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *mockDashboardGuarder) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type scenarioContext struct {
|
type scenarioContext struct {
|
||||||
dashboardGuardianMock *mockDashboardGuarder
|
dashboardGuardianMock *guardian.FakeDashboardGuardian
|
||||||
}
|
}
|
||||||
|
|
||||||
type scenarioFunc func(c *scenarioContext)
|
type scenarioFunc func(c *scenarioContext)
|
||||||
|
|
||||||
func dashboardGuardianScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) {
|
func dashboardGuardianScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
|
||||||
Convey(desc, func() {
|
Convey(desc, func() {
|
||||||
origNewDashboardGuardian := guardian.New
|
origNewDashboardGuardian := guardian.New
|
||||||
mockDashboardGuardian(mock)
|
guardian.MockDashboardGuardian(mock)
|
||||||
|
|
||||||
sc := &scenarioContext{
|
sc := &scenarioContext{
|
||||||
dashboardGuardianMock: mock,
|
dashboardGuardianMock: mock,
|
||||||
@ -861,15 +809,15 @@ func dashboardGuardianScenario(desc string, mock *mockDashboardGuarder, fn scena
|
|||||||
}
|
}
|
||||||
|
|
||||||
type dashboardPermissionScenarioContext struct {
|
type dashboardPermissionScenarioContext struct {
|
||||||
dashboardGuardianMock *mockDashboardGuarder
|
dashboardGuardianMock *guardian.FakeDashboardGuardian
|
||||||
}
|
}
|
||||||
|
|
||||||
type dashboardPermissionScenarioFunc func(sc *dashboardPermissionScenarioContext)
|
type dashboardPermissionScenarioFunc func(sc *dashboardPermissionScenarioContext)
|
||||||
|
|
||||||
func dashboardPermissionScenario(desc string, mock *mockDashboardGuarder, fn dashboardPermissionScenarioFunc) {
|
func dashboardPermissionScenario(desc string, mock *guardian.FakeDashboardGuardian, fn dashboardPermissionScenarioFunc) {
|
||||||
Convey(desc, func() {
|
Convey(desc, func() {
|
||||||
origNewDashboardGuardian := guardian.New
|
origNewDashboardGuardian := guardian.New
|
||||||
mockDashboardGuardian(mock)
|
guardian.MockDashboardGuardian(mock)
|
||||||
|
|
||||||
sc := &dashboardPermissionScenarioContext{
|
sc := &dashboardPermissionScenarioContext{
|
||||||
dashboardGuardianMock: mock,
|
dashboardGuardianMock: mock,
|
||||||
@ -884,8 +832,8 @@ func dashboardPermissionScenario(desc string, mock *mockDashboardGuarder, fn das
|
|||||||
}
|
}
|
||||||
|
|
||||||
func permissionScenario(desc string, canSave bool, fn dashboardPermissionScenarioFunc) {
|
func permissionScenario(desc string, canSave bool, fn dashboardPermissionScenarioFunc) {
|
||||||
mock := &mockDashboardGuarder{
|
mock := &guardian.FakeDashboardGuardian{
|
||||||
canSave: canSave,
|
CanSaveValue: canSave,
|
||||||
}
|
}
|
||||||
dashboardPermissionScenario(desc, mock, fn)
|
dashboardPermissionScenario(desc, mock, fn)
|
||||||
}
|
}
|
||||||
@ -902,10 +850,10 @@ func callSaveWithError(cmd models.SaveDashboardCommand) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func dashboardServiceScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) {
|
func dashboardServiceScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
|
||||||
Convey(desc, func() {
|
Convey(desc, func() {
|
||||||
origNewDashboardGuardian := guardian.New
|
origNewDashboardGuardian := guardian.New
|
||||||
mockDashboardGuardian(mock)
|
guardian.MockDashboardGuardian(mock)
|
||||||
|
|
||||||
sc := &scenarioContext{
|
sc := &scenarioContext{
|
||||||
dashboardGuardianMock: mock,
|
dashboardGuardianMock: mock,
|
||||||
|
@ -3,6 +3,7 @@ package sqlstore
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
@ -124,6 +125,24 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
generateNewUid = util.GenerateShortUid
|
generateNewUid = util.GenerateShortUid
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Should be able to create dashboard", func() {
|
||||||
|
cmd := m.SaveDashboardCommand{
|
||||||
|
OrgId: 1,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"title": "folderId",
|
||||||
|
"tags": []interface{}{},
|
||||||
|
}),
|
||||||
|
UserId: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SaveDashboard(&cmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(cmd.Result.CreatedBy, ShouldEqual, 100)
|
||||||
|
So(cmd.Result.Created.IsZero(), ShouldBeFalse)
|
||||||
|
So(cmd.Result.UpdatedBy, ShouldEqual, 100)
|
||||||
|
So(cmd.Result.Updated.IsZero(), ShouldBeFalse)
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Should be able to update dashboard by id and remove folderId", func() {
|
Convey("Should be able to update dashboard by id and remove folderId", func() {
|
||||||
cmd := m.SaveDashboardCommand{
|
cmd := m.SaveDashboardCommand{
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
@ -134,6 +153,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
Overwrite: true,
|
Overwrite: true,
|
||||||
FolderId: 2,
|
FolderId: 2,
|
||||||
|
UserId: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := SaveDashboard(&cmd)
|
err := SaveDashboard(&cmd)
|
||||||
@ -149,6 +169,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
FolderId: 0,
|
FolderId: 0,
|
||||||
Overwrite: true,
|
Overwrite: true,
|
||||||
|
UserId: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = SaveDashboard(&cmd)
|
err = SaveDashboard(&cmd)
|
||||||
@ -162,6 +183,10 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
err = GetDashboard(&query)
|
err = GetDashboard(&query)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(query.Result.FolderId, ShouldEqual, 0)
|
So(query.Result.FolderId, ShouldEqual, 0)
|
||||||
|
So(query.Result.CreatedBy, ShouldEqual, savedDash.CreatedBy)
|
||||||
|
So(query.Result.Created, ShouldEqual, savedDash.Created.Truncate(time.Second))
|
||||||
|
So(query.Result.UpdatedBy, ShouldEqual, 100)
|
||||||
|
So(query.Result.Updated.IsZero(), ShouldBeFalse)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should be able to delete a dashboard folder and its children", func() {
|
Convey("Should be able to delete a dashboard folder and its children", func() {
|
||||||
|
@ -26,7 +26,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
|||||||
loadStore() {
|
loadStore() {
|
||||||
const { nav, folder, view } = this.props;
|
const { nav, folder, view } = this.props;
|
||||||
return folder.load(view.routeParams.get('uid') as string).then(res => {
|
return folder.load(view.routeParams.get('uid') as string).then(res => {
|
||||||
view.updatePathAndQuery(`${res.meta.url}/permissions`, {}, {});
|
view.updatePathAndQuery(`${res.url}/permissions`, {}, {});
|
||||||
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions');
|
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,17 +9,14 @@ describe('FolderSettings', () => {
|
|||||||
let page;
|
let page;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
backendSrv.getDashboardByUid.mockReturnValue(
|
backendSrv.getFolderByUid.mockReturnValue(
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
dashboard: {
|
id: 1,
|
||||||
id: 1,
|
uid: 'uid',
|
||||||
title: 'Folder Name',
|
title: 'Folder Name',
|
||||||
uid: 'uid-str',
|
url: '/dashboards/f/uid/folder-name',
|
||||||
},
|
canSave: true,
|
||||||
meta: {
|
version: 1,
|
||||||
url: '/dashboards/f/uid/folder-name',
|
|
||||||
canSave: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import appEvents from 'app/core/app_events';
|
|||||||
@observer
|
@observer
|
||||||
export class FolderSettings extends React.Component<IContainerProps, any> {
|
export class FolderSettings extends React.Component<IContainerProps, any> {
|
||||||
formSnapshot: any;
|
formSnapshot: any;
|
||||||
dashboard: any;
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -22,9 +21,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
|||||||
|
|
||||||
return folder.load(view.routeParams.get('uid') as string).then(res => {
|
return folder.load(view.routeParams.get('uid') as string).then(res => {
|
||||||
this.formSnapshot = getSnapshot(folder);
|
this.formSnapshot = getSnapshot(folder);
|
||||||
this.dashboard = res.dashboard;
|
view.updatePathAndQuery(`${res.url}/settings`, {}, {});
|
||||||
|
|
||||||
view.updatePathAndQuery(`${res.meta.url}/settings`, {}, {});
|
|
||||||
|
|
||||||
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
|
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
|
||||||
});
|
});
|
||||||
@ -51,7 +48,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
|||||||
const { nav, folder, view } = this.props;
|
const { nav, folder, view } = this.props;
|
||||||
|
|
||||||
folder
|
folder
|
||||||
.saveFolder(this.dashboard, { overwrite: false })
|
.saveFolder({ overwrite: false })
|
||||||
.then(newUrl => {
|
.then(newUrl => {
|
||||||
view.updatePathAndQuery(newUrl, {}, {});
|
view.updatePathAndQuery(newUrl, {}, {});
|
||||||
|
|
||||||
@ -61,7 +58,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
|
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
|
||||||
})
|
})
|
||||||
.catch(this.handleSaveFolderError);
|
.catch(this.handleSaveFolderError.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(evt) {
|
delete(evt) {
|
||||||
@ -79,7 +76,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
|||||||
icon: 'fa-trash',
|
icon: 'fa-trash',
|
||||||
yesText: 'Delete',
|
yesText: 'Delete',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
return this.props.folder.deleteFolder().then(() => {
|
return folder.deleteFolder().then(() => {
|
||||||
appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]);
|
appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]);
|
||||||
view.updatePathAndQuery('dashboards', '', '');
|
view.updatePathAndQuery('dashboards', '', '');
|
||||||
});
|
});
|
||||||
@ -91,6 +88,8 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
|||||||
if (err.data && err.data.status === 'version-mismatch') {
|
if (err.data && err.data.status === 'version-mismatch') {
|
||||||
err.isHandled = true;
|
err.isHandled = true;
|
||||||
|
|
||||||
|
const { nav, folder, view } = this.props;
|
||||||
|
|
||||||
appEvents.emit('confirm-modal', {
|
appEvents.emit('confirm-modal', {
|
||||||
title: 'Conflict',
|
title: 'Conflict',
|
||||||
text: 'Someone else has updated this folder.',
|
text: 'Someone else has updated this folder.',
|
||||||
@ -98,16 +97,20 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
|||||||
yesText: 'Save & Overwrite',
|
yesText: 'Save & Overwrite',
|
||||||
icon: 'fa-warning',
|
icon: 'fa-warning',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
this.props.folder.saveFolder(this.dashboard, { overwrite: true });
|
folder
|
||||||
|
.saveFolder({ overwrite: true })
|
||||||
|
.then(newUrl => {
|
||||||
|
view.updatePathAndQuery(newUrl, {}, {});
|
||||||
|
|
||||||
|
appEvents.emit('dashboard-saved');
|
||||||
|
appEvents.emit('alert-success', ['Folder saved']);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.data && err.data.status === 'name-exists') {
|
|
||||||
err.isHandled = true;
|
|
||||||
|
|
||||||
appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -53,7 +53,7 @@ describe('AddPermissions', () => {
|
|||||||
wrapper.find('form').simulate('submit', { preventDefault() {} });
|
wrapper.find('form').simulate('submit', { preventDefault() {} });
|
||||||
|
|
||||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
|
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ describe('AddPermissions', () => {
|
|||||||
wrapper.find('form').simulate('submit', { preventDefault() {} });
|
wrapper.find('form').simulate('submit', { preventDefault() {} });
|
||||||
|
|
||||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
|
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -78,8 +78,8 @@ export class ManageDashboardsCtrl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.backendSrv.getDashboardByUid(this.folderUid).then(dash => {
|
return this.backendSrv.getFolderByUid(this.folderUid).then(folder => {
|
||||||
this.canSave = dash.meta.canSave;
|
this.canSave = folder.canSave;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -173,48 +173,13 @@ export class ManageDashboardsCtrl {
|
|||||||
icon: 'fa-trash',
|
icon: 'fa-trash',
|
||||||
yesText: 'Delete',
|
yesText: 'Delete',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
const foldersAndDashboards = data.folders.concat(data.dashboards);
|
this.deleteFoldersAndDashboards(data.folders, data.dashboards);
|
||||||
this.deleteFoldersAndDashboards(foldersAndDashboards);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteFoldersAndDashboards(uids) {
|
private deleteFoldersAndDashboards(folderUids, dashboardUids) {
|
||||||
this.backendSrv.deleteDashboards(uids).then(result => {
|
this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => {
|
||||||
const folders = _.filter(result, dash => dash.meta.isFolder);
|
|
||||||
const folderCount = folders.length;
|
|
||||||
const dashboards = _.filter(result, dash => !dash.meta.isFolder);
|
|
||||||
const dashCount = dashboards.length;
|
|
||||||
|
|
||||||
if (result.length > 0) {
|
|
||||||
let header;
|
|
||||||
let msg;
|
|
||||||
|
|
||||||
if (folderCount > 0 && dashCount > 0) {
|
|
||||||
header = `Folder${folderCount === 1 ? '' : 's'} And Dashboard${dashCount === 1 ? '' : 's'} Deleted`;
|
|
||||||
msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} `;
|
|
||||||
msg += `and ${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`;
|
|
||||||
} else if (folderCount > 0) {
|
|
||||||
header = `Folder${folderCount === 1 ? '' : 's'} Deleted`;
|
|
||||||
|
|
||||||
if (folderCount === 1) {
|
|
||||||
msg = `${folders[0].dashboard.title} has been deleted`;
|
|
||||||
} else {
|
|
||||||
msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} has been deleted`;
|
|
||||||
}
|
|
||||||
} else if (dashCount > 0) {
|
|
||||||
header = `Dashboard${dashCount === 1 ? '' : 's'} Deleted`;
|
|
||||||
|
|
||||||
if (dashCount === 1) {
|
|
||||||
msg = `${dashboards[0].dashboard.title} has been deleted`;
|
|
||||||
} else {
|
|
||||||
msg = `${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appEvents.emit('alert-success', [header, msg]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshList();
|
this.refreshList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -221,14 +221,18 @@ export class BackendSrv {
|
|||||||
return this.get('/api/search', query);
|
return this.get('/api/search', query);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDashboard(type, slug) {
|
getDashboardBySlug(slug) {
|
||||||
return this.get('/api/dashboards/' + type + '/' + slug);
|
return this.get(`/api/dashboards/db/${slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDashboardByUid(uid: string) {
|
getDashboardByUid(uid: string) {
|
||||||
return this.get(`/api/dashboards/uid/${uid}`);
|
return this.get(`/api/dashboards/uid/${uid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFolderByUid(uid: string) {
|
||||||
|
return this.get(`/api/folders/${uid}`);
|
||||||
|
}
|
||||||
|
|
||||||
saveDashboard(dash, options) {
|
saveDashboard(dash, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
@ -240,55 +244,41 @@ export class BackendSrv {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createDashboardFolder(name) {
|
createFolder(payload: any) {
|
||||||
const dash = {
|
return this.post('/api/folders', payload);
|
||||||
schemaVersion: 16,
|
|
||||||
title: name.trim(),
|
|
||||||
editable: true,
|
|
||||||
panels: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.post('/api/dashboards/db/', {
|
|
||||||
dashboard: dash,
|
|
||||||
isFolder: true,
|
|
||||||
overwrite: false,
|
|
||||||
}).then(res => {
|
|
||||||
return this.getDashboard('db', res.slug);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveFolder(dash, options) {
|
updateFolder(folder, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
return this.post('/api/dashboards/db/', {
|
return this.put(`/api/folders/${folder.uid}`, {
|
||||||
dashboard: dash,
|
title: folder.title,
|
||||||
isFolder: true,
|
version: folder.version,
|
||||||
overwrite: options.overwrite === true,
|
overwrite: options.overwrite === true,
|
||||||
message: options.message || '',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteDashboard(uid) {
|
deleteFolder(uid: string, showSuccessAlert) {
|
||||||
let deferred = this.$q.defer();
|
return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true });
|
||||||
|
|
||||||
this.getDashboardByUid(uid).then(fullDash => {
|
|
||||||
this.delete(`/api/dashboards/uid/${uid}`)
|
|
||||||
.then(() => {
|
|
||||||
deferred.resolve(fullDash);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
deferred.reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteDashboards(dashboardUids) {
|
deleteDashboard(uid, showSuccessAlert) {
|
||||||
|
return this.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/api/dashboards/uid/${uid}`,
|
||||||
|
showSuccessAlert: showSuccessAlert === true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFoldersAndDashboards(folderUids, dashboardUids) {
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
|
|
||||||
for (let uid of dashboardUids) {
|
for (let folderUid of folderUids) {
|
||||||
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, uid));
|
tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let dashboardUid of dashboardUids) {
|
||||||
|
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.executeInOrder(tasks, []);
|
return this.executeInOrder(tasks, []);
|
||||||
|
@ -18,9 +18,9 @@ export class CreateFolderCtrl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.backendSrv.createDashboardFolder(this.title).then(result => {
|
return this.backendSrv.createFolder({ title: this.title }).then(result => {
|
||||||
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
||||||
this.$location.url(locationUtil.stripBaseFromUrl(result.meta.url));
|
this.$location.url(locationUtil.stripBaseFromUrl(result.url));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export class FolderDashboardsCtrl {
|
|||||||
const loader = new FolderPageLoader(this.backendSrv);
|
const loader = new FolderPageLoader(this.backendSrv);
|
||||||
|
|
||||||
loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => {
|
loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => {
|
||||||
const url = locationUtil.stripBaseFromUrl(folder.meta.url);
|
const url = locationUtil.stripBaseFromUrl(folder.url);
|
||||||
|
|
||||||
if (url !== $location.path()) {
|
if (url !== $location.path()) {
|
||||||
$location.path(url).replace();
|
$location.path(url).replace();
|
||||||
|
@ -36,16 +36,16 @@ export class FolderPageLoader {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.backendSrv.getDashboardByUid(uid).then(result => {
|
return this.backendSrv.getFolderByUid(uid).then(folder => {
|
||||||
ctrl.folderId = result.dashboard.id;
|
ctrl.folderId = folder.id;
|
||||||
const folderTitle = result.dashboard.title;
|
const folderTitle = folder.title;
|
||||||
const folderUrl = result.meta.url;
|
const folderUrl = folder.url;
|
||||||
ctrl.navModel.main.text = folderTitle;
|
ctrl.navModel.main.text = folderTitle;
|
||||||
|
|
||||||
const dashTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-dashboards');
|
const dashTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-dashboards');
|
||||||
dashTab.url = folderUrl;
|
dashTab.url = folderUrl;
|
||||||
|
|
||||||
if (result.meta.canAdmin) {
|
if (folder.canAdmin) {
|
||||||
const permTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-permissions');
|
const permTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-permissions');
|
||||||
permTab.url = folderUrl + '/permissions';
|
permTab.url = folderUrl + '/permissions';
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ export class FolderPageLoader {
|
|||||||
ctrl.navModel.main.children = [dashTab];
|
ctrl.navModel.main.children = [dashTab];
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return folder;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,13 +89,13 @@ export class FolderPickerCtrl {
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.backendSrv.createDashboardFolder(this.newFolderName).then(result => {
|
return this.backendSrv.createFolder({ title: this.newFolderName }).then(result => {
|
||||||
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
||||||
|
|
||||||
this.closeCreateFolder();
|
this.closeCreateFolder();
|
||||||
this.folder = {
|
this.folder = {
|
||||||
text: result.dashboard.title,
|
text: result.title,
|
||||||
value: result.dashboard.id,
|
value: result.id,
|
||||||
};
|
};
|
||||||
this.onFolderChange(this.folder);
|
this.onFolderChange(this.folder);
|
||||||
});
|
});
|
||||||
|
@ -7,8 +7,7 @@ export class FolderSettingsCtrl {
|
|||||||
folderId: number;
|
folderId: number;
|
||||||
uid: string;
|
uid: string;
|
||||||
canSave = false;
|
canSave = false;
|
||||||
dashboard: any;
|
folder: any;
|
||||||
meta: any;
|
|
||||||
title: string;
|
title: string;
|
||||||
hasChanged: boolean;
|
hasChanged: boolean;
|
||||||
|
|
||||||
@ -23,10 +22,9 @@ export class FolderSettingsCtrl {
|
|||||||
$location.path(`${folder.meta.url}/settings`).replace();
|
$location.path(`${folder.meta.url}/settings`).replace();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dashboard = folder.dashboard;
|
this.folder = folder;
|
||||||
this.meta = folder.meta;
|
this.canSave = this.folder.canSave;
|
||||||
this.canSave = folder.meta.canSave;
|
this.title = this.folder.title;
|
||||||
this.title = this.dashboard.title;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,10 +36,10 @@ export class FolderSettingsCtrl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dashboard.title = this.title.trim();
|
this.folder.title = this.title.trim();
|
||||||
|
|
||||||
return this.backendSrv
|
return this.backendSrv
|
||||||
.updateDashboardFolder(this.dashboard, { overwrite: false })
|
.updateFolder(this.folder)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.url !== this.$location.path()) {
|
if (result.url !== this.$location.path()) {
|
||||||
this.$location.url(result.url + '/settings');
|
this.$location.url(result.url + '/settings');
|
||||||
@ -54,7 +52,7 @@ export class FolderSettingsCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
titleChanged() {
|
titleChanged() {
|
||||||
this.hasChanged = this.dashboard.title.toLowerCase() !== this.title.trim().toLowerCase();
|
this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(evt) {
|
delete(evt) {
|
||||||
@ -69,8 +67,8 @@ export class FolderSettingsCtrl {
|
|||||||
icon: 'fa-trash',
|
icon: 'fa-trash',
|
||||||
yesText: 'Delete',
|
yesText: 'Delete',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
return this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => {
|
return this.backendSrv.deleteFolder(this.uid).then(() => {
|
||||||
appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]);
|
appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]);
|
||||||
this.$location.url('dashboards');
|
this.$location.url('dashboards');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -88,15 +86,9 @@ export class FolderSettingsCtrl {
|
|||||||
yesText: 'Save & Overwrite',
|
yesText: 'Save & Overwrite',
|
||||||
icon: 'fa-warning',
|
icon: 'fa-warning',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
this.backendSrv.updateDashboardFolder(this.dashboard, { overwrite: true });
|
this.backendSrv.updateFolder(this.folder, { overwrite: true });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.data && err.data.status === 'name-exists') {
|
|
||||||
err.isHandled = true;
|
|
||||||
|
|
||||||
appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,7 +374,7 @@ describe('DashboardModel', function() {
|
|||||||
|
|
||||||
it('should assign id', function() {
|
it('should assign id', function() {
|
||||||
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
|
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
|
||||||
model.rows[0].panels[0] = { };
|
model.rows[0].panels[0] = {};
|
||||||
|
|
||||||
let dashboard = new DashboardModel(model);
|
let dashboard = new DashboardModel(model);
|
||||||
expect(dashboard.panels[0].id).toBe(1);
|
expect(dashboard.panels[0].id).toBe(1);
|
||||||
|
@ -18,7 +18,7 @@ export class SoloPanelCtrl {
|
|||||||
|
|
||||||
// if no uid, redirect to new route based on slug
|
// if no uid, redirect to new route based on slug
|
||||||
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
||||||
backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
|
backendSrv.getDashboardBySlug($routeParams.slug).then(res => {
|
||||||
if (res) {
|
if (res) {
|
||||||
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
|
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
|
||||||
$location.path(url).replace();
|
$location.path(url).replace();
|
||||||
|
@ -21,7 +21,7 @@ export class LoadDashboardCtrl {
|
|||||||
|
|
||||||
// if no uid, redirect to new route based on slug
|
// if no uid, redirect to new route based on slug
|
||||||
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
||||||
backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
|
backendSrv.getDashboardBySlug($routeParams.slug).then(res => {
|
||||||
if (res) {
|
if (res) {
|
||||||
$location.path(locationUtil.stripBaseFromUrl(res.meta.url)).replace();
|
$location.path(locationUtil.stripBaseFromUrl(res.meta.url)).replace();
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,12 @@ import { types, getEnv, flow } from 'mobx-state-tree';
|
|||||||
|
|
||||||
export const Folder = types.model('Folder', {
|
export const Folder = types.model('Folder', {
|
||||||
id: types.identifier(types.number),
|
id: types.identifier(types.number),
|
||||||
|
uid: types.string,
|
||||||
title: types.string,
|
title: types.string,
|
||||||
url: types.string,
|
url: types.string,
|
||||||
canSave: types.boolean,
|
canSave: types.boolean,
|
||||||
uid: types.string,
|
|
||||||
hasChanged: types.boolean,
|
hasChanged: types.boolean,
|
||||||
|
version: types.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FolderStore = types
|
export const FolderStore = types
|
||||||
@ -21,15 +22,15 @@ export const FolderStore = types
|
|||||||
}
|
}
|
||||||
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
const res = yield backendSrv.getDashboardByUid(uid);
|
const res = yield backendSrv.getFolderByUid(uid);
|
||||||
|
|
||||||
self.folder = Folder.create({
|
self.folder = Folder.create({
|
||||||
id: res.dashboard.id,
|
id: res.id,
|
||||||
title: res.dashboard.title,
|
uid: res.uid,
|
||||||
url: res.meta.url,
|
title: res.title,
|
||||||
uid: res.dashboard.uid,
|
url: res.url,
|
||||||
canSave: res.meta.canSave,
|
canSave: res.canSave,
|
||||||
hasChanged: false,
|
hasChanged: false,
|
||||||
|
version: res.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
@ -40,12 +41,13 @@ export const FolderStore = types
|
|||||||
self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0;
|
self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
saveFolder: flow(function* saveFolder(dashboard: any, options: any) {
|
saveFolder: flow(function* saveFolder(options: any) {
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
dashboard.title = self.folder.title.trim();
|
self.folder.title = self.folder.title.trim();
|
||||||
|
|
||||||
const res = yield backendSrv.saveFolder(dashboard, options);
|
const res = yield backendSrv.updateFolder(self.folder, options);
|
||||||
self.folder.url = res.url;
|
self.folder.url = res.url;
|
||||||
|
self.folder.version = res.version;
|
||||||
|
|
||||||
return `${self.folder.url}/settings`;
|
return `${self.folder.url}/settings`;
|
||||||
}),
|
}),
|
||||||
@ -53,6 +55,6 @@ export const FolderStore = types
|
|||||||
deleteFolder: flow(function* deleteFolder() {
|
deleteFolder: flow(function* deleteFolder() {
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
|
||||||
return backendSrv.deleteDashboard(self.folder.uid);
|
return backendSrv.deleteFolder(self.folder.uid);
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
@ -44,7 +44,7 @@ describe('PermissionsStore', () => {
|
|||||||
expect(store.items[0].permission).toBe(2);
|
expect(store.items[0].permission).toBe(2);
|
||||||
expect(store.items[0].permissionName).toBe('Edit');
|
expect(store.items[0].permissionName).toBe('Edit');
|
||||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
|
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save removed permissions automatically', () => {
|
it('should save removed permissions automatically', () => {
|
||||||
@ -54,7 +54,7 @@ describe('PermissionsStore', () => {
|
|||||||
|
|
||||||
expect(store.items.length).toBe(2);
|
expect(store.items.length).toBe(2);
|
||||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
|
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when duplicate team permissions are added', () => {
|
describe('when duplicate team permissions are added', () => {
|
||||||
|
@ -110,7 +110,7 @@ export const PermissionsStore = types
|
|||||||
self.dashboardId = dashboardId;
|
self.dashboardId = dashboardId;
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
|
|
||||||
const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`);
|
const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/permissions`);
|
||||||
const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot);
|
const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot);
|
||||||
self.items = items;
|
self.items = items;
|
||||||
self.originalItems = items;
|
self.originalItems = items;
|
||||||
@ -210,7 +210,7 @@ const updateItems = self => {
|
|||||||
|
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/acl`, {
|
res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, {
|
||||||
items: updated,
|
items: updated,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,290 +1,254 @@
|
|||||||
@import "font_awesome";
|
@import 'font_awesome';
|
||||||
@import "grafana_icons";
|
@import 'grafana_icons';
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto"), local("Roboto-Regular"),
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
url(../fonts/roboto/ek4gzZ-GeXAPcSbHtCeQI_esZW2xOQ-xsNqO47m55DA.woff2)
|
url(../fonts/roboto/ek4gzZ-GeXAPcSbHtCeQI_esZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F;
|
unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F;
|
||||||
}
|
}
|
||||||
/* cyrillic */
|
/* cyrillic */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto"), local("Roboto-Regular"),
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
url(../fonts/roboto/mErvLBYg_cXG3rLvUsKT_fesZW2xOQ-xsNqO47m55DA.woff2)
|
url(../fonts/roboto/mErvLBYg_cXG3rLvUsKT_fesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116;
|
unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116;
|
||||||
}
|
}
|
||||||
/* greek-ext */
|
/* greek-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto"), local("Roboto-Regular"),
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
url(../fonts/roboto/-2n2p-_Y08sg57CNWQfKNvesZW2xOQ-xsNqO47m55DA.woff2)
|
url(../fonts/roboto/-2n2p-_Y08sg57CNWQfKNvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+1f00-1fff;
|
unicode-range: U+1f00-1fff;
|
||||||
}
|
}
|
||||||
/* greek */
|
/* greek */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto"), local("Roboto-Regular"),
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
url(../fonts/roboto/u0TOpm082MNkS5K0Q4rhqvesZW2xOQ-xsNqO47m55DA.woff2)
|
url(../fonts/roboto/u0TOpm082MNkS5K0Q4rhqvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0370-03ff;
|
unicode-range: U+0370-03ff;
|
||||||
}
|
}
|
||||||
/* vietnamese */
|
/* vietnamese */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto"), local("Roboto-Regular"),
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
url(../fonts/roboto/NdF9MtnOpLzo-noMoG0miPesZW2xOQ-xsNqO47m55DA.woff2)
|
url(../fonts/roboto/NdF9MtnOpLzo-noMoG0miPesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab;
|
unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab;
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto"), local("Roboto-Regular"),
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
url(../fonts/roboto/Fcx7Wwv8OzT71A3E1XOAjvesZW2xOQ-xsNqO47m55DA.woff2)
|
url(../fonts/roboto/Fcx7Wwv8OzT71A3E1XOAjvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
|
||||||
format("woff2");
|
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, U+A720-A7FF;
|
||||||
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f,
|
|
||||||
U+A720-A7FF;
|
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto"), local("Roboto-Regular"),
|
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/roboto/CWB0XYA8bzo0kSThX0UTuA.woff2) format('woff2');
|
||||||
url(../fonts/roboto/CWB0XYA8bzo0kSThX0UTuA.woff2) format("woff2");
|
unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc, U+2000-206f, U+2074, U+20ac, U+2212, U+2215;
|
||||||
unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc,
|
|
||||||
U+2000-206f, U+2074, U+20ac, U+2212, U+2215;
|
|
||||||
}
|
}
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium"), local("Roboto-Medium"),
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
url(../fonts/roboto/ZLqKeelYbATG60EpZBSDyxJtnKITppOI_IvcXXDNrsc.woff2)
|
url(../fonts/roboto/ZLqKeelYbATG60EpZBSDyxJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F;
|
unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F;
|
||||||
}
|
}
|
||||||
/* cyrillic */
|
/* cyrillic */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium"), local("Roboto-Medium"),
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
url(../fonts/roboto/oHi30kwQWvpCWqAhzHcCSBJtnKITppOI_IvcXXDNrsc.woff2)
|
url(../fonts/roboto/oHi30kwQWvpCWqAhzHcCSBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116;
|
unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116;
|
||||||
}
|
}
|
||||||
/* greek-ext */
|
/* greek-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium"), local("Roboto-Medium"),
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
url(../fonts/roboto/rGvHdJnr2l75qb0YND9NyBJtnKITppOI_IvcXXDNrsc.woff2)
|
url(../fonts/roboto/rGvHdJnr2l75qb0YND9NyBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+1f00-1fff;
|
unicode-range: U+1f00-1fff;
|
||||||
}
|
}
|
||||||
/* greek */
|
/* greek */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium"), local("Roboto-Medium"),
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
url(../fonts/roboto/mx9Uck6uB63VIKFYnEMXrRJtnKITppOI_IvcXXDNrsc.woff2)
|
url(../fonts/roboto/mx9Uck6uB63VIKFYnEMXrRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0370-03ff;
|
unicode-range: U+0370-03ff;
|
||||||
}
|
}
|
||||||
/* vietnamese */
|
/* vietnamese */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium"), local("Roboto-Medium"),
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
url(../fonts/roboto/mbmhprMH69Zi6eEPBYVFhRJtnKITppOI_IvcXXDNrsc.woff2)
|
url(../fonts/roboto/mbmhprMH69Zi6eEPBYVFhRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab;
|
unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab;
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium"), local("Roboto-Medium"),
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
url(../fonts/roboto/oOeFwZNlrTefzLYmlVV1UBJtnKITppOI_IvcXXDNrsc.woff2)
|
url(../fonts/roboto/oOeFwZNlrTefzLYmlVV1UBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
|
||||||
format("woff2");
|
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, U+A720-A7FF;
|
||||||
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f,
|
|
||||||
U+A720-A7FF;
|
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium"), local("Roboto-Medium"),
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
url(../fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2)
|
url(../fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2) format('woff2');
|
||||||
format("woff2");
|
unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc, U+2000-206f, U+2074, U+20ac, U+2212, U+2215;
|
||||||
unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc,
|
|
||||||
U+2000-206f, U+2074, U+20ac, U+2212, U+2215;
|
|
||||||
}
|
}
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto Italic"), local("Roboto-Italic"),
|
src: local('Roboto Italic'), local('Roboto-Italic'),
|
||||||
url(../fonts/roboto/WxrXJa0C3KdtC7lMafG4dRTbgVql8nDJpwnrE27mub0.woff2)
|
url(../fonts/roboto/WxrXJa0C3KdtC7lMafG4dRTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F;
|
unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F;
|
||||||
}
|
}
|
||||||
/* cyrillic */
|
/* cyrillic */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto Italic"), local("Roboto-Italic"),
|
src: local('Roboto Italic'), local('Roboto-Italic'),
|
||||||
url(../fonts/roboto/OpXUqTo0UgQQhGj_SFdLWBTbgVql8nDJpwnrE27mub0.woff2)
|
url(../fonts/roboto/OpXUqTo0UgQQhGj_SFdLWBTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116;
|
unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116;
|
||||||
}
|
}
|
||||||
/* greek-ext */
|
/* greek-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto Italic"), local("Roboto-Italic"),
|
src: local('Roboto Italic'), local('Roboto-Italic'),
|
||||||
url(../fonts/roboto/1hZf02POANh32k2VkgEoUBTbgVql8nDJpwnrE27mub0.woff2)
|
url(../fonts/roboto/1hZf02POANh32k2VkgEoUBTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+1f00-1fff;
|
unicode-range: U+1f00-1fff;
|
||||||
}
|
}
|
||||||
/* greek */
|
/* greek */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto Italic"), local("Roboto-Italic"),
|
src: local('Roboto Italic'), local('Roboto-Italic'),
|
||||||
url(../fonts/roboto/cDKhRaXnQTOVbaoxwdOr9xTbgVql8nDJpwnrE27mub0.woff2)
|
url(../fonts/roboto/cDKhRaXnQTOVbaoxwdOr9xTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0370-03ff;
|
unicode-range: U+0370-03ff;
|
||||||
}
|
}
|
||||||
/* vietnamese */
|
/* vietnamese */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto Italic"), local("Roboto-Italic"),
|
src: local('Roboto Italic'), local('Roboto-Italic'),
|
||||||
url(../fonts/roboto/K23cxWVTrIFD6DJsEVi07RTbgVql8nDJpwnrE27mub0.woff2)
|
url(../fonts/roboto/K23cxWVTrIFD6DJsEVi07RTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab;
|
unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab;
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto Italic"), local("Roboto-Italic"),
|
src: local('Roboto Italic'), local('Roboto-Italic'),
|
||||||
url(../fonts/roboto/vSzulfKSK0LLjjfeaxcREhTbgVql8nDJpwnrE27mub0.woff2)
|
url(../fonts/roboto/vSzulfKSK0LLjjfeaxcREhTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
|
||||||
format("woff2");
|
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, U+A720-A7FF;
|
||||||
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f,
|
|
||||||
U+A720-A7FF;
|
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local("Roboto Italic"), local("Roboto-Italic"),
|
src: local('Roboto Italic'), local('Roboto-Italic'),
|
||||||
url(../fonts/roboto/vPcynSL0qHq_6dX7lKVByfesZW2xOQ-xsNqO47m55DA.woff2)
|
url(../fonts/roboto/vPcynSL0qHq_6dX7lKVByfesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
|
||||||
format("woff2");
|
unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc, U+2000-206f, U+2074, U+20ac, U+2212, U+2215;
|
||||||
unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc,
|
|
||||||
U+2000-206f, U+2074, U+20ac, U+2212, U+2215;
|
|
||||||
}
|
}
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"),
|
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
|
||||||
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0TTOQ_MqJVwkKsUn0wKzc2I.woff2)
|
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0TTOQ_MqJVwkKsUn0wKzc2I.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F;
|
unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F;
|
||||||
}
|
}
|
||||||
/* cyrillic */
|
/* cyrillic */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"),
|
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
|
||||||
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0TUj_cnvWIuuBMVgbX098Mw.woff2)
|
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0TUj_cnvWIuuBMVgbX098Mw.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116;
|
unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116;
|
||||||
}
|
}
|
||||||
/* greek-ext */
|
/* greek-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"),
|
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
|
||||||
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0UbcKLIaa1LC45dFaAfauRA.woff2)
|
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0UbcKLIaa1LC45dFaAfauRA.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+1f00-1fff;
|
unicode-range: U+1f00-1fff;
|
||||||
}
|
}
|
||||||
/* greek */
|
/* greek */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"),
|
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
|
||||||
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0Wo_sUJ8uO4YLWRInS22T3Y.woff2)
|
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0Wo_sUJ8uO4YLWRInS22T3Y.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0370-03ff;
|
unicode-range: U+0370-03ff;
|
||||||
}
|
}
|
||||||
/* vietnamese */
|
/* vietnamese */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"),
|
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
|
||||||
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0b6up8jxqWt8HVA3mDhkV_0.woff2)
|
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0b6up8jxqWt8HVA3mDhkV_0.woff2) format('woff2');
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab;
|
unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab;
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"),
|
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
|
||||||
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0SYE0-AqJ3nfInTTiDXDjU4.woff2)
|
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0SYE0-AqJ3nfInTTiDXDjU4.woff2) format('woff2');
|
||||||
format("woff2");
|
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, U+A720-A7FF;
|
||||||
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f,
|
|
||||||
U+A720-A7FF;
|
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: 'Roboto';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"),
|
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
|
||||||
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0Y4P5ICox8Kq3LLUNMylGO4.woff2)
|
url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0Y4P5ICox8Kq3LLUNMylGO4.woff2) format('woff2');
|
||||||
format("woff2");
|
unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc, U+2000-206f, U+2074, U+20ac, U+2212, U+2215;
|
||||||
unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc,
|
|
||||||
U+2000-206f, U+2074, U+20ac, U+2212, U+2215;
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ export const backendSrv = {
|
|||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
getDashboard: jest.fn(),
|
getDashboard: jest.fn(),
|
||||||
getDashboardByUid: jest.fn(),
|
getDashboardByUid: jest.fn(),
|
||||||
|
getFolderByUid: jest.fn(),
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
78
tests/api/folder.test.ts
Normal file
78
tests/api/folder.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import client from './client';
|
||||||
|
import * as setup from './setup';
|
||||||
|
|
||||||
|
describe('/api/folders', () => {
|
||||||
|
let state: any = {};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
state = await setup.ensureState({
|
||||||
|
orgName: 'api-test-org',
|
||||||
|
users: [
|
||||||
|
{ user: setup.admin, role: 'Admin' },
|
||||||
|
{ user: setup.editor, role: 'Editor' },
|
||||||
|
{ user: setup.viewer, role: 'Viewer' },
|
||||||
|
],
|
||||||
|
admin: setup.admin,
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
title: 'Folder 1',
|
||||||
|
uid: 'f-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Folder 2',
|
||||||
|
uid: 'f-02',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Folder 3',
|
||||||
|
uid: 'f-03',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With admin user', () => {
|
||||||
|
it('can delete folder', async () => {
|
||||||
|
let rsp = await client.callAs(setup.admin).delete(`/api/folders/f-01`);
|
||||||
|
expect(rsp.data.title).toBe('Folder 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can update folder', async () => {
|
||||||
|
let rsp = await client.callAs(setup.admin).put(`/api/folders/f-02`, {
|
||||||
|
uid: 'f-02',
|
||||||
|
title: 'Folder 2 upd',
|
||||||
|
overwrite: true,
|
||||||
|
});
|
||||||
|
expect(rsp.data.title).toBe('Folder 2 upd');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can update folder uid', async () => {
|
||||||
|
let rsp = await client.callAs(setup.admin).put(`/api/folders/f-03`, {
|
||||||
|
uid: 'f-03-upd',
|
||||||
|
title: 'Folder 3 upd',
|
||||||
|
overwrite: true,
|
||||||
|
});
|
||||||
|
expect(rsp.data.uid).toBe('f-03-upd');
|
||||||
|
expect(rsp.data.title).toBe('Folder 3 upd');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With viewer user', () => {
|
||||||
|
it('Cannot delete folder', async () => {
|
||||||
|
let rsp = await setup.expectError(() => {
|
||||||
|
return client.callAs(setup.viewer).delete(`/api/folders/f-02`);
|
||||||
|
});
|
||||||
|
expect(rsp.response.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Cannot update folder', async () => {
|
||||||
|
let rsp = await setup.expectError(() => {
|
||||||
|
return client.callAs(setup.viewer).put(`/api/folders/f-02`, {
|
||||||
|
uid: 'f-02',
|
||||||
|
title: 'Folder 2 upd',
|
||||||
|
overwrite: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(rsp.response.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -90,6 +90,18 @@ export async function createDashboard(user, dashboard) {
|
|||||||
return dashboard;
|
return dashboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createFolder(user, folder) {
|
||||||
|
const rsp = await client.callAs(user).post(`/api/folders`, {
|
||||||
|
uid: folder.uid,
|
||||||
|
title: folder.title,
|
||||||
|
overwrite: true,
|
||||||
|
});
|
||||||
|
folder.id = rsp.id;
|
||||||
|
folder.url = rsp.url;
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureState(state) {
|
export async function ensureState(state) {
|
||||||
const org = await getOrg(state.orgName);
|
const org = await getOrg(state.orgName);
|
||||||
|
|
||||||
@ -99,9 +111,13 @@ export async function ensureState(state) {
|
|||||||
await setUsingOrg(user, org);
|
await setUsingOrg(user, org);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let dashboard of state.dashboards) {
|
for (let dashboard of state.dashboards || []) {
|
||||||
await createDashboard(state.admin, dashboard);
|
await createDashboard(state.admin, dashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let folder of state.folders || []) {
|
||||||
|
await createFolder(state.admin, folder);
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user