mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into docs_v5.0
This commit is contained in:
commit
6751bbc17f
27
ROADMAP.md
27
ROADMAP.md
@ -1,25 +1,28 @@
|
||||
# Roadmap (2017-10-31)
|
||||
# Roadmap (2018-02-22)
|
||||
|
||||
This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change.
|
||||
But it will give you an idea of our current vision and plan.
|
||||
|
||||
### Short term (1-4 months)
|
||||
### Short term (1-2 months)
|
||||
|
||||
- Release Grafana v5
|
||||
- Teams
|
||||
- Dashboard folders
|
||||
- Dashboard & folder permissions (assigned to users or groups)
|
||||
- New Dashboard layout engine
|
||||
- New sidemenu & nav UX
|
||||
- v5.1
|
||||
- Crossplatform builds & build speed improvements
|
||||
- Enterprise LDAP
|
||||
- New template interpolation syntax
|
||||
- Provisioning workflow
|
||||
- First login registration view
|
||||
- IFQL Initial support
|
||||
|
||||
### Mid term (2-4 months)
|
||||
|
||||
- v5.2
|
||||
- Azure monitor backend rewrite
|
||||
- Elasticsearch alerting
|
||||
- React migration foundation (core components)
|
||||
- Graphite 1.1 Tags Support
|
||||
- Backend plugins? (alert notifiers, auth)
|
||||
|
||||
### Long term (4 - 8 months)
|
||||
|
||||
- Backend plugins to support more Auth options, Alerting data sources & notifications
|
||||
- Alerting improvements (silence, per series tracking, etc)
|
||||
- Dashboard as configuration and other automation / provisioning improvements
|
||||
- Progress on React migration
|
||||
- Change visualization (panel type) on the fly.
|
||||
- Multi stat panel (vertical version of singlestat with bars/graph mode with big number etc)
|
||||
|
25
docker/blocks/prometheus2/docker-compose.yaml
Normal file
25
docker/blocks/prometheus2/docker-compose.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
prometheus:
|
||||
build: blocks/prometheus2
|
||||
network_mode: host
|
||||
ports:
|
||||
- "9090:9090"
|
||||
|
||||
node_exporter:
|
||||
image: prom/node-exporter
|
||||
network_mode: host
|
||||
ports:
|
||||
- "9100:9100"
|
||||
|
||||
fake-prometheus-data:
|
||||
image: grafana/fake-data-gen
|
||||
network_mode: host
|
||||
ports:
|
||||
- "9091:9091"
|
||||
environment:
|
||||
FD_DATASOURCE: prom
|
||||
|
||||
alertmanager:
|
||||
image: quay.io/prometheus/alertmanager
|
||||
network_mode: host
|
||||
ports:
|
||||
- "9093:9093"
|
@ -246,6 +246,24 @@ func (hs *HttpServer) registerRoutes() {
|
||||
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
|
||||
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
|
||||
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
|
||||
@ -266,9 +284,9 @@ func (hs *HttpServer) registerRoutes() {
|
||||
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
|
||||
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
|
||||
dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
|
||||
aclRoute.Get("/", wrap(GetDashboardAclList))
|
||||
aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
|
||||
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute RouteRegister) {
|
||||
dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList))
|
||||
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 {
|
||||
return nil, ApiError(404, "Dashboard not found", err)
|
||||
}
|
||||
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
@ -166,8 +167,10 @@ func DeleteDashboard(c *middleware.Context) Response {
|
||||
return ApiError(500, "Failed to delete dashboard", err)
|
||||
}
|
||||
|
||||
var resp = map[string]interface{}{"title": dash.Title}
|
||||
return Json(200, resp)
|
||||
return Json(200, util.DynMap{
|
||||
"title": dash.Title,
|
||||
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteDashboardByUid(c *middleware.Context) Response {
|
||||
@ -186,8 +189,10 @@ func DeleteDashboardByUid(c *middleware.Context) Response {
|
||||
return ApiError(500, "Failed to delete dashboard", err)
|
||||
}
|
||||
|
||||
var resp = map[string]interface{}{"title": dash.Title}
|
||||
return Json(200, resp)
|
||||
return Json(200, util.DynMap{
|
||||
"title": dash.Title,
|
||||
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
||||
})
|
||||
}
|
||||
|
||||
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
)
|
||||
|
||||
func GetDashboardAclList(c *middleware.Context) Response {
|
||||
func GetDashboardPermissionList(c *middleware.Context) Response {
|
||||
dashId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
|
||||
@ -26,7 +26,7 @@ func GetDashboardAclList(c *middleware.Context) Response {
|
||||
|
||||
acl, err := guardian.GetAcl()
|
||||
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 {
|
||||
@ -38,7 +38,7 @@ func GetDashboardAclList(c *middleware.Context) Response {
|
||||
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")
|
||||
|
||||
_, 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 ApiSuccess("Dashboard acl updated")
|
||||
return ApiSuccess("Dashboard permissions updated")
|
||||
}
|
@ -12,8 +12,8 @@ import (
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
Convey("Given a dashboard acl", t, func() {
|
||||
func TestDashboardPermissionApiEndpoint(t *testing.T) {
|
||||
Convey("Given a dashboard with permissions", t, func() {
|
||||
mockResult := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
|
||||
{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
|
||||
|
||||
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() {
|
||||
sc.handlerFunc = GetDashboardAclList
|
||||
sc.handlerFunc = GetDashboardPermissionList
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
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
|
||||
sc.handlerFunc = GetDashboardAclList
|
||||
sc.handlerFunc = GetDashboardPermissionList
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
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
|
||||
CallPostAcl(sc)
|
||||
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() {
|
||||
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})
|
||||
|
||||
Convey("Should be able to access ACL", func() {
|
||||
sc.handlerFunc = GetDashboardAclList
|
||||
sc.handlerFunc = GetDashboardPermissionList
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
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})
|
||||
|
||||
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})
|
||||
|
||||
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() {
|
||||
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})
|
||||
|
||||
// Getting the permissions is an Admin permission
|
||||
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()
|
||||
|
||||
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() {
|
||||
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() {
|
||||
sc.handlerFunc = GetDashboardAclList
|
||||
sc.handlerFunc = GetDashboardPermissionList
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
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.OrgRole = role
|
||||
|
||||
return UpdateDashboardAcl(c, cmd)
|
||||
return UpdateDashboardPermissions(c, cmd)
|
||||
})
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
@ -746,8 +746,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
CallGetDashboard(sc)
|
||||
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
@ -758,6 +757,11 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta
|
||||
return dash
|
||||
}
|
||||
|
||||
func CallGetDashboard(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallGetDashboardVersion(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
|
||||
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 (
|
||||
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.")
|
||||
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
|
||||
@ -45,7 +47,8 @@ type DashboardAcl struct {
|
||||
|
||||
type DashboardAclInfoDTO struct {
|
||||
OrgId int64 `json:"-"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
DashboardId int64 `json:"dashboardId,omitempty"`
|
||||
FolderId int64 `json:"folderId,omitempty"`
|
||||
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
// Typed errors
|
||||
var (
|
||||
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")
|
||||
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")
|
||||
@ -112,9 +112,9 @@ func NewDashboard(title string) *Dashboard {
|
||||
// NewDashboardFolder creates a new dashboard folder
|
||||
func NewDashboardFolder(title string) *Dashboard {
|
||||
folder := NewDashboard(title)
|
||||
folder.IsFolder = true
|
||||
folder.Data.Set("schemaVersion", 16)
|
||||
folder.Data.Set("editable", true)
|
||||
folder.Data.Set("hideControls", true)
|
||||
folder.Data.Set("version", 0)
|
||||
folder.IsFolder = true
|
||||
return folder
|
||||
}
|
||||
@ -166,10 +166,6 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
||||
userId = -1
|
||||
}
|
||||
|
||||
if dash.Data.Get("version").MustInt(0) == 0 {
|
||||
dash.CreatedBy = userId
|
||||
}
|
||||
|
||||
dash.UpdatedBy = userId
|
||||
dash.OrgId = cmd.OrgId
|
||||
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
|
||||
}
|
||||
|
||||
type dashboardServiceImpl struct{}
|
||||
type dashboardServiceImpl struct {
|
||||
orgId int64
|
||||
user *models.SignedInUser
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||
cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
|
||||
@ -53,7 +56,7 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
|
||||
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.Title = strings.TrimSpace(dash.Title)
|
||||
@ -78,13 +81,15 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO)
|
||||
return nil, models.ErrDashboardUidToLong
|
||||
}
|
||||
|
||||
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
|
||||
OrgId: dto.OrgId,
|
||||
Dashboard: dash,
|
||||
}
|
||||
if validateAlerts {
|
||||
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
|
||||
OrgId: dto.OrgId,
|
||||
Dashboard: dash,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||
return nil, models.ErrDashboardContainsInvalidAlertData
|
||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||
return nil, models.ErrDashboardContainsInvalidAlertData
|
||||
}
|
||||
}
|
||||
|
||||
validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
|
||||
@ -142,7 +147,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
}
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto)
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -172,7 +177,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
}
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto)
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -191,7 +196,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto)
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ func TestDashboardService(t *testing.T) {
|
||||
service := dashboardServiceImpl{}
|
||||
|
||||
origNewDashboardGuardian := guardian.New
|
||||
mockDashboardGuardian(&fakeDashboardGuardian{canSave: true})
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
||||
|
||||
Convey("Save dashboard validation", func() {
|
||||
dto := &SaveDashboardDTO{}
|
||||
@ -72,7 +72,7 @@ func TestDashboardService(t *testing.T) {
|
||||
dto.Dashboard.SetUid(tc.Uid)
|
||||
dto.User = &models.SignedInUser{}
|
||||
|
||||
_, err := service.buildSaveDashboardCommand(dto)
|
||||
_, err := service.buildSaveDashboardCommand(dto, true)
|
||||
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
|
||||
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 {
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
userId := cmd.UserId
|
||||
|
||||
if userId == 0 {
|
||||
userId = -1
|
||||
}
|
||||
|
||||
if dash.Id > 0 {
|
||||
var existing m.Dashboard
|
||||
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 {
|
||||
dash.SetVersion(1)
|
||||
dash.Created = time.Now()
|
||||
dash.CreatedBy = userId
|
||||
dash.Updated = time.Now()
|
||||
dash.UpdatedBy = userId
|
||||
metrics.M_Api_Dashboard_Insert.Inc()
|
||||
affectedRows, err = sess.Insert(dash)
|
||||
} else {
|
||||
v := dash.Version
|
||||
v++
|
||||
dash.SetVersion(v)
|
||||
dash.SetVersion(dash.Version + 1)
|
||||
|
||||
if !cmd.UpdatedAt.IsZero() {
|
||||
dash.Updated = cmd.UpdatedAt
|
||||
} else {
|
||||
dash.Updated = time.Now()
|
||||
}
|
||||
|
||||
dash.UpdatedBy = userId
|
||||
|
||||
affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash)
|
||||
}
|
||||
|
||||
@ -514,7 +526,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
|
||||
}
|
||||
|
||||
if !folderExists {
|
||||
return m.ErrFolderNotFound
|
||||
return m.ErrDashboardFolderNotFound
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,9 +142,9 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
So(sc.dashboardGuardianMock.dashId, ShouldEqual, 0)
|
||||
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
|
||||
So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
|
||||
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||
})
|
||||
})
|
||||
|
||||
@ -165,9 +165,9 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
So(sc.dashboardGuardianMock.dashId, ShouldEqual, otherSavedFolder.Id)
|
||||
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
|
||||
So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
|
||||
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||
})
|
||||
})
|
||||
|
||||
@ -189,9 +189,9 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInGeneralFolder.Id)
|
||||
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
|
||||
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedDashInGeneralFolder.Id)
|
||||
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||
})
|
||||
})
|
||||
|
||||
@ -213,9 +213,9 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInFolder.Id)
|
||||
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
|
||||
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedDashInFolder.Id)
|
||||
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||
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() {
|
||||
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 {
|
||||
dashboardGuardianMock *mockDashboardGuarder
|
||||
dashboardGuardianMock *guardian.FakeDashboardGuardian
|
||||
}
|
||||
|
||||
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() {
|
||||
origNewDashboardGuardian := guardian.New
|
||||
mockDashboardGuardian(mock)
|
||||
guardian.MockDashboardGuardian(mock)
|
||||
|
||||
sc := &scenarioContext{
|
||||
dashboardGuardianMock: mock,
|
||||
@ -861,15 +809,15 @@ func dashboardGuardianScenario(desc string, mock *mockDashboardGuarder, fn scena
|
||||
}
|
||||
|
||||
type dashboardPermissionScenarioContext struct {
|
||||
dashboardGuardianMock *mockDashboardGuarder
|
||||
dashboardGuardianMock *guardian.FakeDashboardGuardian
|
||||
}
|
||||
|
||||
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() {
|
||||
origNewDashboardGuardian := guardian.New
|
||||
mockDashboardGuardian(mock)
|
||||
guardian.MockDashboardGuardian(mock)
|
||||
|
||||
sc := &dashboardPermissionScenarioContext{
|
||||
dashboardGuardianMock: mock,
|
||||
@ -884,8 +832,8 @@ func dashboardPermissionScenario(desc string, mock *mockDashboardGuarder, fn das
|
||||
}
|
||||
|
||||
func permissionScenario(desc string, canSave bool, fn dashboardPermissionScenarioFunc) {
|
||||
mock := &mockDashboardGuarder{
|
||||
canSave: canSave,
|
||||
mock := &guardian.FakeDashboardGuardian{
|
||||
CanSaveValue: canSave,
|
||||
}
|
||||
dashboardPermissionScenario(desc, mock, fn)
|
||||
}
|
||||
@ -902,10 +850,10 @@ func callSaveWithError(cmd models.SaveDashboardCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func dashboardServiceScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) {
|
||||
func dashboardServiceScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
|
||||
Convey(desc, func() {
|
||||
origNewDashboardGuardian := guardian.New
|
||||
mockDashboardGuardian(mock)
|
||||
guardian.MockDashboardGuardian(mock)
|
||||
|
||||
sc := &scenarioContext{
|
||||
dashboardGuardianMock: mock,
|
||||
|
@ -3,6 +3,7 @@ package sqlstore
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -124,6 +125,24 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
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() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
@ -134,6 +153,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
}),
|
||||
Overwrite: true,
|
||||
FolderId: 2,
|
||||
UserId: 100,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
@ -149,6 +169,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
}),
|
||||
FolderId: 0,
|
||||
Overwrite: true,
|
||||
UserId: 100,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&cmd)
|
||||
@ -162,6 +183,10 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
err = GetDashboard(&query)
|
||||
So(err, ShouldBeNil)
|
||||
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() {
|
||||
|
@ -26,7 +26,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
loadStore() {
|
||||
const { nav, folder, view } = this.props;
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
@ -9,17 +9,14 @@ describe('FolderSettings', () => {
|
||||
let page;
|
||||
|
||||
beforeAll(() => {
|
||||
backendSrv.getDashboardByUid.mockReturnValue(
|
||||
backendSrv.getFolderByUid.mockReturnValue(
|
||||
Promise.resolve({
|
||||
dashboard: {
|
||||
id: 1,
|
||||
title: 'Folder Name',
|
||||
uid: 'uid-str',
|
||||
},
|
||||
meta: {
|
||||
url: '/dashboards/f/uid/folder-name',
|
||||
canSave: true,
|
||||
},
|
||||
id: 1,
|
||||
uid: 'uid',
|
||||
title: 'Folder Name',
|
||||
url: '/dashboards/f/uid/folder-name',
|
||||
canSave: true,
|
||||
version: 1,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -10,7 +10,6 @@ import appEvents from 'app/core/app_events';
|
||||
@observer
|
||||
export class FolderSettings extends React.Component<IContainerProps, any> {
|
||||
formSnapshot: any;
|
||||
dashboard: any;
|
||||
|
||||
constructor(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 => {
|
||||
this.formSnapshot = getSnapshot(folder);
|
||||
this.dashboard = res.dashboard;
|
||||
|
||||
view.updatePathAndQuery(`${res.meta.url}/settings`, {}, {});
|
||||
view.updatePathAndQuery(`${res.url}/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;
|
||||
|
||||
folder
|
||||
.saveFolder(this.dashboard, { overwrite: false })
|
||||
.saveFolder({ overwrite: false })
|
||||
.then(newUrl => {
|
||||
view.updatePathAndQuery(newUrl, {}, {});
|
||||
|
||||
@ -61,7 +58,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
||||
.then(() => {
|
||||
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
|
||||
})
|
||||
.catch(this.handleSaveFolderError);
|
||||
.catch(this.handleSaveFolderError.bind(this));
|
||||
}
|
||||
|
||||
delete(evt) {
|
||||
@ -79,7 +76,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: () => {
|
||||
return this.props.folder.deleteFolder().then(() => {
|
||||
return folder.deleteFolder().then(() => {
|
||||
appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]);
|
||||
view.updatePathAndQuery('dashboards', '', '');
|
||||
});
|
||||
@ -91,6 +88,8 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
||||
if (err.data && err.data.status === 'version-mismatch') {
|
||||
err.isHandled = true;
|
||||
|
||||
const { nav, folder, view } = this.props;
|
||||
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Conflict',
|
||||
text: 'Someone else has updated this folder.',
|
||||
@ -98,16 +97,20 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
||||
yesText: 'Save & Overwrite',
|
||||
icon: 'fa-warning',
|
||||
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() {
|
||||
|
@ -53,7 +53,7 @@ describe('AddPermissions', () => {
|
||||
wrapper.find('form').simulate('submit', { preventDefault() {} });
|
||||
|
||||
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() {} });
|
||||
|
||||
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 this.backendSrv.getDashboardByUid(this.folderUid).then(dash => {
|
||||
this.canSave = dash.meta.canSave;
|
||||
return this.backendSrv.getFolderByUid(this.folderUid).then(folder => {
|
||||
this.canSave = folder.canSave;
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -173,48 +173,13 @@ export class ManageDashboardsCtrl {
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: () => {
|
||||
const foldersAndDashboards = data.folders.concat(data.dashboards);
|
||||
this.deleteFoldersAndDashboards(foldersAndDashboards);
|
||||
this.deleteFoldersAndDashboards(data.folders, data.dashboards);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private deleteFoldersAndDashboards(uids) {
|
||||
this.backendSrv.deleteDashboards(uids).then(result => {
|
||||
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]);
|
||||
}
|
||||
|
||||
private deleteFoldersAndDashboards(folderUids, dashboardUids) {
|
||||
this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => {
|
||||
this.refreshList();
|
||||
});
|
||||
}
|
||||
|
@ -221,14 +221,18 @@ export class BackendSrv {
|
||||
return this.get('/api/search', query);
|
||||
}
|
||||
|
||||
getDashboard(type, slug) {
|
||||
return this.get('/api/dashboards/' + type + '/' + slug);
|
||||
getDashboardBySlug(slug) {
|
||||
return this.get(`/api/dashboards/db/${slug}`);
|
||||
}
|
||||
|
||||
getDashboardByUid(uid: string) {
|
||||
return this.get(`/api/dashboards/uid/${uid}`);
|
||||
}
|
||||
|
||||
getFolderByUid(uid: string) {
|
||||
return this.get(`/api/folders/${uid}`);
|
||||
}
|
||||
|
||||
saveDashboard(dash, options) {
|
||||
options = options || {};
|
||||
|
||||
@ -240,55 +244,41 @@ export class BackendSrv {
|
||||
});
|
||||
}
|
||||
|
||||
createDashboardFolder(name) {
|
||||
const dash = {
|
||||
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);
|
||||
});
|
||||
createFolder(payload: any) {
|
||||
return this.post('/api/folders', payload);
|
||||
}
|
||||
|
||||
saveFolder(dash, options) {
|
||||
updateFolder(folder, options) {
|
||||
options = options || {};
|
||||
|
||||
return this.post('/api/dashboards/db/', {
|
||||
dashboard: dash,
|
||||
isFolder: true,
|
||||
return this.put(`/api/folders/${folder.uid}`, {
|
||||
title: folder.title,
|
||||
version: folder.version,
|
||||
overwrite: options.overwrite === true,
|
||||
message: options.message || '',
|
||||
});
|
||||
}
|
||||
|
||||
deleteDashboard(uid) {
|
||||
let deferred = this.$q.defer();
|
||||
|
||||
this.getDashboardByUid(uid).then(fullDash => {
|
||||
this.delete(`/api/dashboards/uid/${uid}`)
|
||||
.then(() => {
|
||||
deferred.resolve(fullDash);
|
||||
})
|
||||
.catch(err => {
|
||||
deferred.reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
deleteFolder(uid: string, showSuccessAlert) {
|
||||
return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true });
|
||||
}
|
||||
|
||||
deleteDashboards(dashboardUids) {
|
||||
deleteDashboard(uid, showSuccessAlert) {
|
||||
return this.request({
|
||||
method: 'DELETE',
|
||||
url: `/api/dashboards/uid/${uid}`,
|
||||
showSuccessAlert: showSuccessAlert === true,
|
||||
});
|
||||
}
|
||||
|
||||
deleteFoldersAndDashboards(folderUids, dashboardUids) {
|
||||
const tasks = [];
|
||||
|
||||
for (let uid of dashboardUids) {
|
||||
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, uid));
|
||||
for (let folderUid of folderUids) {
|
||||
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, []);
|
||||
|
@ -463,6 +463,15 @@ kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2);
|
||||
kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
|
||||
kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3);
|
||||
|
||||
// Hash Rate
|
||||
kbn.valueFormats.Hs = kbn.formatBuilders.decimalSIPrefix('H/s');
|
||||
kbn.valueFormats.KHs = kbn.formatBuilders.decimalSIPrefix('H/s', 1);
|
||||
kbn.valueFormats.MHs = kbn.formatBuilders.decimalSIPrefix('H/s', 2);
|
||||
kbn.valueFormats.GHs = kbn.formatBuilders.decimalSIPrefix('H/s', 3);
|
||||
kbn.valueFormats.THs = kbn.formatBuilders.decimalSIPrefix('H/s', 4);
|
||||
kbn.valueFormats.PHs = kbn.formatBuilders.decimalSIPrefix('H/s', 5);
|
||||
kbn.valueFormats.EHs = kbn.formatBuilders.decimalSIPrefix('H/s', 6);
|
||||
|
||||
// Throughput
|
||||
kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops');
|
||||
kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps');
|
||||
@ -878,6 +887,18 @@ kbn.getUnitFormats = function() {
|
||||
{ text: 'gigabits/sec', value: 'Gbits' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'hash rate',
|
||||
submenu: [
|
||||
{text: 'hashes/sec', value: 'Hs'},
|
||||
{text: 'kilohashes/sec', value: 'KHs'},
|
||||
{text: 'megahashes/sec', value: 'MHs'},
|
||||
{text: 'gigahashes/sec', value: 'GHs'},
|
||||
{text: 'terahashes/sec', value: 'THs'},
|
||||
{text: 'petahashes/sec', value: 'PHs'},
|
||||
{text: 'exahashes/sec', value: 'EHs'},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'throughput',
|
||||
submenu: [
|
||||
|
@ -18,9 +18,9 @@ export class CreateFolderCtrl {
|
||||
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']);
|
||||
this.$location.url(locationUtil.stripBaseFromUrl(result.meta.url));
|
||||
this.$location.url(locationUtil.stripBaseFromUrl(result.url));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ export class DashboardSrv {
|
||||
|
||||
save(clone, options) {
|
||||
options = options || {};
|
||||
options.folderId = options.folderId || this.dash.meta.folderId || clone.folderId;
|
||||
options.folderId = options.folderId >= 0 ? options.folderId : this.dash.meta.folderId || clone.folderId;
|
||||
|
||||
return this.backendSrv
|
||||
.saveDashboard(clone, options)
|
||||
|
@ -14,7 +14,7 @@ export class FolderDashboardsCtrl {
|
||||
const loader = new FolderPageLoader(this.backendSrv);
|
||||
|
||||
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()) {
|
||||
$location.path(url).replace();
|
||||
|
@ -36,16 +36,16 @@ export class FolderPageLoader {
|
||||
},
|
||||
};
|
||||
|
||||
return this.backendSrv.getDashboardByUid(uid).then(result => {
|
||||
ctrl.folderId = result.dashboard.id;
|
||||
const folderTitle = result.dashboard.title;
|
||||
const folderUrl = result.meta.url;
|
||||
return this.backendSrv.getFolderByUid(uid).then(folder => {
|
||||
ctrl.folderId = folder.id;
|
||||
const folderTitle = folder.title;
|
||||
const folderUrl = folder.url;
|
||||
ctrl.navModel.main.text = folderTitle;
|
||||
|
||||
const dashTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-dashboards');
|
||||
dashTab.url = folderUrl;
|
||||
|
||||
if (result.meta.canAdmin) {
|
||||
if (folder.canAdmin) {
|
||||
const permTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-permissions');
|
||||
permTab.url = folderUrl + '/permissions';
|
||||
|
||||
@ -55,7 +55,7 @@ export class FolderPageLoader {
|
||||
ctrl.navModel.main.children = [dashTab];
|
||||
}
|
||||
|
||||
return result;
|
||||
return folder;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -89,13 +89,13 @@ export class FolderPickerCtrl {
|
||||
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']);
|
||||
|
||||
this.closeCreateFolder();
|
||||
this.folder = {
|
||||
text: result.dashboard.title,
|
||||
value: result.dashboard.id,
|
||||
text: result.title,
|
||||
value: result.id,
|
||||
};
|
||||
this.onFolderChange(this.folder);
|
||||
});
|
||||
|
@ -7,8 +7,7 @@ export class FolderSettingsCtrl {
|
||||
folderId: number;
|
||||
uid: string;
|
||||
canSave = false;
|
||||
dashboard: any;
|
||||
meta: any;
|
||||
folder: any;
|
||||
title: string;
|
||||
hasChanged: boolean;
|
||||
|
||||
@ -23,10 +22,9 @@ export class FolderSettingsCtrl {
|
||||
$location.path(`${folder.meta.url}/settings`).replace();
|
||||
}
|
||||
|
||||
this.dashboard = folder.dashboard;
|
||||
this.meta = folder.meta;
|
||||
this.canSave = folder.meta.canSave;
|
||||
this.title = this.dashboard.title;
|
||||
this.folder = folder;
|
||||
this.canSave = this.folder.canSave;
|
||||
this.title = this.folder.title;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -38,10 +36,10 @@ export class FolderSettingsCtrl {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dashboard.title = this.title.trim();
|
||||
this.folder.title = this.title.trim();
|
||||
|
||||
return this.backendSrv
|
||||
.updateDashboardFolder(this.dashboard, { overwrite: false })
|
||||
.updateFolder(this.folder)
|
||||
.then(result => {
|
||||
if (result.url !== this.$location.path()) {
|
||||
this.$location.url(result.url + '/settings');
|
||||
@ -54,7 +52,7 @@ export class FolderSettingsCtrl {
|
||||
}
|
||||
|
||||
titleChanged() {
|
||||
this.hasChanged = this.dashboard.title.toLowerCase() !== this.title.trim().toLowerCase();
|
||||
this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase();
|
||||
}
|
||||
|
||||
delete(evt) {
|
||||
@ -69,8 +67,8 @@ export class FolderSettingsCtrl {
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: () => {
|
||||
return this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => {
|
||||
appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]);
|
||||
return this.backendSrv.deleteFolder(this.uid).then(() => {
|
||||
appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]);
|
||||
this.$location.url('dashboards');
|
||||
});
|
||||
},
|
||||
@ -88,15 +86,9 @@ export class FolderSettingsCtrl {
|
||||
yesText: 'Save & Overwrite',
|
||||
icon: 'fa-warning',
|
||||
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.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ const template = `
|
||||
<div class="gf-form">
|
||||
<folder-picker initial-folder-id="ctrl.folderId"
|
||||
on-change="ctrl.onFolderChange($folder)"
|
||||
enter-folder-creation="ctrl.onEnterFolderCreation()"
|
||||
exit-folder-creation="ctrl.onExitFolderCreation()"
|
||||
enable-create-new="true"
|
||||
label-class="width-7">
|
||||
</folder-picker>
|
||||
@ -29,7 +31,7 @@ const template = `
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row text-center">
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.save()" ng-disabled="!ctrl.isValidFolderSelection">Save</button>
|
||||
<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@ -40,6 +42,7 @@ export class SaveDashboardAsModalCtrl {
|
||||
clone: any;
|
||||
folderId: any;
|
||||
dismiss: () => void;
|
||||
isValidFolderSelection = true;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private dashboardSrv) {
|
||||
@ -79,6 +82,14 @@ export class SaveDashboardAsModalCtrl {
|
||||
onFolderChange(folder) {
|
||||
this.folderId = folder.id;
|
||||
}
|
||||
|
||||
onEnterFolderCreation() {
|
||||
this.isValidFolderSelection = false;
|
||||
}
|
||||
|
||||
onExitFolderCreation() {
|
||||
this.isValidFolderSelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveDashboardAsDirective() {
|
||||
|
@ -18,7 +18,7 @@ export class SoloPanelCtrl {
|
||||
|
||||
// if no uid, redirect to new route based on slug
|
||||
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) {
|
||||
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
|
||||
$location.path(url).replace();
|
||||
|
@ -101,6 +101,7 @@
|
||||
get-options="getSizeOptions()"
|
||||
on-change="onChangeInternal()"
|
||||
label-mode="true"
|
||||
allow-custom="true"
|
||||
css-class="width-12">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
|
@ -21,7 +21,7 @@ export class LoadDashboardCtrl {
|
||||
|
||||
// if no uid, redirect to new route based on slug
|
||||
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) {
|
||||
$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', {
|
||||
id: types.identifier(types.number),
|
||||
uid: types.string,
|
||||
title: types.string,
|
||||
url: types.string,
|
||||
canSave: types.boolean,
|
||||
uid: types.string,
|
||||
hasChanged: types.boolean,
|
||||
version: types.number,
|
||||
});
|
||||
|
||||
export const FolderStore = types
|
||||
@ -21,15 +22,15 @@ export const FolderStore = types
|
||||
}
|
||||
|
||||
const backendSrv = getEnv(self).backendSrv;
|
||||
const res = yield backendSrv.getDashboardByUid(uid);
|
||||
|
||||
const res = yield backendSrv.getFolderByUid(uid);
|
||||
self.folder = Folder.create({
|
||||
id: res.dashboard.id,
|
||||
title: res.dashboard.title,
|
||||
url: res.meta.url,
|
||||
uid: res.dashboard.uid,
|
||||
canSave: res.meta.canSave,
|
||||
id: res.id,
|
||||
uid: res.uid,
|
||||
title: res.title,
|
||||
url: res.url,
|
||||
canSave: res.canSave,
|
||||
hasChanged: false,
|
||||
version: res.version,
|
||||
});
|
||||
|
||||
return res;
|
||||
@ -40,12 +41,13 @@ export const FolderStore = types
|
||||
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;
|
||||
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.version = res.version;
|
||||
|
||||
return `${self.folder.url}/settings`;
|
||||
}),
|
||||
@ -53,6 +55,6 @@ export const FolderStore = types
|
||||
deleteFolder: flow(function* deleteFolder() {
|
||||
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].permissionName).toBe('Edit');
|
||||
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', () => {
|
||||
@ -54,7 +54,7 @@ describe('PermissionsStore', () => {
|
||||
|
||||
expect(store.items.length).toBe(2);
|
||||
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', () => {
|
||||
|
@ -110,7 +110,7 @@ export const PermissionsStore = types
|
||||
self.dashboardId = dashboardId;
|
||||
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);
|
||||
self.items = items;
|
||||
self.originalItems = items;
|
||||
@ -210,7 +210,7 @@ const updateItems = self => {
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/acl`, {
|
||||
res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, {
|
||||
items: updated,
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -75,7 +75,7 @@
|
||||
border-radius: 6px;
|
||||
width: 6px;
|
||||
/* there must be 'right' for ps__thumb-y */
|
||||
right: 2px;
|
||||
right: 0px;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ export const backendSrv = {
|
||||
get: jest.fn(),
|
||||
getDashboard: jest.fn(),
|
||||
getDashboardByUid: jest.fn(),
|
||||
getFolderByUid: 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const org = await getOrg(state.orgName);
|
||||
|
||||
@ -99,9 +111,13 @@ export async function ensureState(state) {
|
||||
await setUsingOrg(user, org);
|
||||
}
|
||||
|
||||
for (let dashboard of state.dashboards) {
|
||||
for (let dashboard of state.dashboards || []) {
|
||||
await createDashboard(state.admin, dashboard);
|
||||
}
|
||||
|
||||
for (let folder of state.folders || []) {
|
||||
await createFolder(state.admin, folder);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user