Merge branch 'develop' into panel-title-menu-ux

This commit is contained in:
Torkel Ödegaard 2017-08-02 09:56:08 +02:00
commit 9fb60c2fc8
116 changed files with 5196 additions and 1071 deletions

View File

@ -132,6 +132,18 @@ func (hs *HttpServer) registerRoutes() {
r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
}, reqGrafanaAdmin)
// user group (admin permission required)
r.Group("/user-groups", func() {
r.Get("/:userGroupId", wrap(GetUserGroupById))
r.Get("/search", wrap(SearchUserGroups))
r.Post("/", quota("user-groups"), bind(m.CreateUserGroupCommand{}), wrap(CreateUserGroup))
r.Put("/:userGroupId", bind(m.UpdateUserGroupCommand{}), wrap(UpdateUserGroup))
r.Delete("/:userGroupId", wrap(DeleteUserGroupById))
r.Get("/:userGroupId/members", wrap(GetUserGroupMembers))
r.Post("/:userGroupId/members", quota("user-groups"), bind(m.AddUserGroupMemberCommand{}), wrap(AddUserGroupMember))
r.Delete("/:userGroupId/members/:userId", wrap(RemoveUserGroupMember))
}, reqOrgAdmin)
// org information available to all users.
r.Group("/org", func() {
r.Get("/", wrap(GetOrgCurrent))
@ -222,19 +234,25 @@ func (hs *HttpServer) registerRoutes() {
// Dashboard
r.Group("/dashboards", func() {
r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
r.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
r.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
r.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
r.Combo("/db/:slug").Get(wrap(GetDashboard)).Delete(wrap(DeleteDashboard))
r.Post("/db", bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
r.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
r.Get("/file/:file", GetDashboardFromJsonFile)
r.Get("/home", wrap(GetHomeDashboard))
r.Get("/tags", GetDashboardTags)
r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
r.Group("/id/:dashboardId", func() {
r.Get("/versions", wrap(GetDashboardVersions))
r.Get("/versions/:id", wrap(GetDashboardVersion))
r.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
r.Group("/acl", func() {
r.Get("/", wrap(GetDashboardAclList))
r.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
r.Delete("/:aclId", wrap(DeleteDashboardAcl))
})
}, reqSignedIn)
})
// Dashboard snapshots

View File

@ -5,7 +5,6 @@ import (
"fmt"
"os"
"path"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
@ -17,7 +16,7 @@ import (
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@ -35,23 +34,34 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
return query.Result, nil
}
func GetDashboard(c *middleware.Context) {
slug := strings.ToLower(c.Params(":slug"))
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
err := bus.Dispatch(&query)
func dashboardGuardianResponse(err error) Response {
if err != nil {
c.JsonApiErr(404, "Dashboard not found", nil)
return
return ApiError(500, "Error while checking dashboard permissions", err)
} else {
return ApiError(403, "Access denied to this dashboard", nil)
}
}
func GetDashboard(c *middleware.Context) Response {
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
if rsp != nil {
return rsp
}
isStarred, err := isDashboardStarredByUser(c, query.Result.Id)
if err != nil {
c.JsonApiErr(500, "Error while checking if dashboard was starred by user", err)
return
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canView, err := guardian.CanView(); err != nil || !canView {
fmt.Printf("%v", err)
return dashboardGuardianResponse(err)
}
dash := query.Result
canEdit, _ := guardian.CanEdit()
canSave, _ := guardian.CanSave()
canAdmin, _ := guardian.CanAdmin()
isStarred, err := isDashboardStarredByUser(c, dash.Id)
if err != nil {
return ApiError(500, "Error while checking if dashboard was starred by user", err)
}
// Finding creator and last updater of the dashboard
updater, creator := "Anonymous", "Anonymous"
@ -62,29 +72,44 @@ func GetDashboard(c *middleware.Context) {
creator = getUserLogin(dash.CreatedBy)
}
meta := dtos.DashboardMeta{
IsStarred: isStarred,
Slug: dash.Slug,
Type: m.DashTypeDB,
CanStar: c.IsSignedIn,
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
Created: dash.Created,
Updated: dash.Updated,
UpdatedBy: updater,
CreatedBy: creator,
Version: dash.Version,
HasAcl: dash.HasAcl,
IsFolder: dash.IsFolder,
FolderId: dash.FolderId,
FolderTitle: "Root",
}
// lookup folder title
if dash.FolderId > 0 {
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Dashboard folder could not be read", err)
}
meta.FolderTitle = query.Result.Title
}
// make sure db version is in sync with json model version
dash.Data.Set("version", dash.Version)
dto := dtos.DashboardFullWithMeta{
Dashboard: dash.Data,
Meta: dtos.DashboardMeta{
IsStarred: isStarred,
Slug: slug,
Type: m.DashTypeDB,
CanStar: c.IsSignedIn,
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
CanEdit: canEditDashboard(c.OrgRole),
Created: dash.Created,
Updated: dash.Updated,
UpdatedBy: updater,
CreatedBy: creator,
Version: dash.Version,
},
Meta: meta,
}
// TODO(ben): copy this performance metrics logic for the new API endpoints added
c.TimeRequest(metrics.M_Api_Dashboard_Get)
c.JSON(200, dto)
return Json(200, dto)
}
func getUserLogin(userId int64) string {
@ -98,24 +123,32 @@ func getUserLogin(userId int64) string {
}
}
func DeleteDashboard(c *middleware.Context) {
slug := c.Params(":slug")
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) {
query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(404, "Dashboard not found", nil)
return
return nil, ApiError(404, "Dashboard not found", err)
}
return query.Result, nil
}
func DeleteDashboard(c *middleware.Context) Response {
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
if rsp != nil {
return rsp
}
cmd := m.DeleteDashboardCommand{Slug: slug, OrgId: c.OrgId}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to delete dashboard", err)
return
return ApiError(500, "Failed to delete dashboard", err)
}
var resp = map[string]interface{}{"title": query.Result.Title}
c.JSON(200, resp)
var resp = map[string]interface{}{"title": dash.Title}
return Json(200, resp)
}
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
@ -124,6 +157,22 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
dash := cmd.GetDashboardModel()
// look up existing dashboard
if dash.Id > 0 {
if existing, _ := getDashboardHelper(c.OrgId, "", dash.Id); existing != nil {
dash.HasAcl = existing.HasAcl
}
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
if dash.IsFolder && dash.FolderId > 0 {
return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
}
// Check if Title is empty
if dash.Title == "" {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
@ -182,11 +231,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
}
c.TimeRequest(metrics.M_Api_Dashboard_Save)
return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
}
func canEditDashboard(role m.RoleType) bool {
return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version, "id": cmd.Result.Id})
}
func GetHomeDashboard(c *middleware.Context) Response {
@ -214,7 +259,9 @@ func GetHomeDashboard(c *middleware.Context) Response {
dash := dtos.DashboardFullWithMeta{}
dash.Meta.IsHome = true
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_READ_ONLY_EDITOR)
dash.Meta.FolderTitle = "Root"
jsonParser := json.NewDecoder(file)
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
return ApiError(500, "Failed to load home dashboard", err)
@ -242,41 +289,24 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
row.Set("panels", panels)
}
func GetDashboardFromJsonFile(c *middleware.Context) {
file := c.Params(":file")
dashboard := search.GetDashboardFromJsonIndex(file)
if dashboard == nil {
c.JsonApiErr(404, "Dashboard not found", nil)
return
}
dash := dtos.DashboardFullWithMeta{Dashboard: dashboard.Data}
dash.Meta.Type = m.DashTypeJson
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
c.JSON(200, &dash)
}
// GetDashboardVersions returns all dashboard versions as JSON
func GetDashboardVersions(c *middleware.Context) Response {
dashboardId := c.ParamsInt64(":dashboardId")
limit := c.QueryInt("limit")
start := c.QueryInt("start")
dashId := c.ParamsInt64(":dashboardId")
if limit == 0 {
limit = 1000
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
query := m.GetDashboardVersionsQuery{
OrgId: c.OrgId,
DashboardId: dashboardId,
Limit: limit,
Start: start,
DashboardId: dashId,
Limit: c.QueryInt("limit"),
Start: c.QueryInt("start"),
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashboardId), err)
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err)
}
for _, version := range query.Result {
@ -300,17 +330,21 @@ func GetDashboardVersions(c *middleware.Context) Response {
// GetDashboardVersion returns the dashboard version with the given ID.
func GetDashboardVersion(c *middleware.Context) Response {
dashboardId := c.ParamsInt64(":dashboardId")
version := c.ParamsInt(":id")
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
query := m.GetDashboardVersionQuery{
OrgId: c.OrgId,
DashboardId: dashboardId,
Version: version,
DashboardId: dashId,
Version: c.ParamsInt(":id"),
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", version, dashboardId), err)
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err)
}
creator := "Anonymous"
@ -361,19 +395,21 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff
// RestoreDashboardVersion restores a dashboard to the given version.
func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response {
dashboardId := c.ParamsInt64(":dashboardId")
dashQuery := m.GetDashboardQuery{Id: dashboardId, OrgId: c.OrgId}
if err := bus.Dispatch(&dashQuery); err != nil {
return ApiError(404, "Dashboard not found", nil)
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"))
if rsp != nil {
return rsp
}
versionQuery := m.GetDashboardVersionQuery{DashboardId: dashboardId, Version: apiCmd.Version, OrgId: c.OrgId}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
if err := bus.Dispatch(&versionQuery); err != nil {
return ApiError(404, "Dashboard version not found", nil)
}
dashboard := dashQuery.Result
version := versionQuery.Result
saveCmd := m.SaveDashboardCommand{}
@ -381,7 +417,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
saveCmd.OrgId = c.OrgId
saveCmd.UserId = c.UserId
saveCmd.Dashboard = version.Data
saveCmd.Dashboard.Set("version", dashboard.Version)
saveCmd.Dashboard.Set("version", dash.Version)
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
return PostDashboard(c, saveCmd)

81
pkg/api/dashboard_acl.go Normal file
View File

@ -0,0 +1,81 @@
package api
import (
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetDashboardAclList(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
acl, err := guardian.GetAcl()
if err != nil {
return ApiError(500, "Failed to get dashboard acl", err)
}
return Json(200, acl)
}
func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.UpdateDashboardAclCommand{}
cmd.DashboardId = dashId
for _, item := range apiCmd.Items {
cmd.Items = append(cmd.Items, &m.DashboardAcl{
OrgId: c.OrgId,
DashboardId: dashId,
UserId: item.UserId,
UserGroupId: item.UserGroupId,
Role: item.Role,
Permission: item.Permission,
Created: time.Now(),
Updated: time.Now(),
})
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
return ApiError(409, err.Error(), err)
}
return ApiError(500, "Failed to create permission", err)
}
metrics.M_Api_Dashboard_Acl_Update.Inc(1)
return ApiSuccess("Dashboard acl updated")
}
func DeleteDashboardAcl(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
aclId := c.ParamsInt64(":aclId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete permission for user", err)
}
return Json(200, "")
}

View File

@ -0,0 +1,174 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardAclApiEndpoint(t *testing.T) {
Convey("Given a dashboard acl", t, func() {
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permission: m.PERMISSION_VIEW},
{Id: 5, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permission: m.PERMISSION_ADMIN},
}
dtoRes := transformDashboardAclsToDTOs(mockResult)
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = dtoRes
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
userGroupResp := []*m.UserGroup{}
bus.AddHandler("test", func(query *m.GetUserGroupsByUserQuery) error {
query.Result = userGroupResp
return nil
})
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) {
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 5)
So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
})
})
})
Convey("When user is 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) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is a member of a user group in the ACL with admin permission", func() {
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
userGroupResp = append(userGroupResp, &m.UserGroup{Id: 2, OrgId: 1, Name: "UG2"})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
Convey("When user is editor 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_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
Convey("Should not be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be not be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is 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) {
Convey("Should not be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be not be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
})
}
func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardAclInfoDTO {
dtos := make([]*m.DashboardAclInfoDTO, 0)
for _, acl := range acls {
dto := &m.DashboardAclInfoDTO{
Id: acl.Id,
OrgId: acl.OrgId,
DashboardId: acl.DashboardId,
Permission: acl.Permission,
UserId: acl.UserId,
UserGroupId: acl.UserGroupId,
}
dtos = append(dtos, dto)
}
return dtos
}

507
pkg/api/dashboard_test.go Normal file
View File

@ -0,0 +1,507 @@
package api
import (
"encoding/json"
"path/filepath"
"testing"
macaron "gopkg.in/macaron.v1"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardApiEndpoint(t *testing.T) {
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
fakeDash := m.NewDashboard("Child dash")
fakeDash.Id = 1
fakeDash.FolderId = 1
fakeDash.HasAcl = false
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetUserGroupsByUserQuery) error {
query.Result = []*m.UserGroup{}
return nil
})
cmd := m.SaveDashboardCommand{
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
"id": fakeDash.Id,
}),
}
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should not be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Read Only Editor", func() {
role := m.ROLE_READ_ONLY_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should be able to view but not save the dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("When saving a dashboard folder in another folder", func() {
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
query.Result.IsFolder = true
return nil
})
invalidCmd := m.SaveDashboardCommand{
FolderId: fakeDash.FolderId,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
}),
}
Convey("Should return an error", func() {
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
})
})
})
})
Convey("Given a dashboard with a parent folder which has an acl", t, func() {
fakeDash := m.NewDashboard("Child dash")
fakeDash.Id = 1
fakeDash.FolderId = 1
fakeDash.HasAcl = true
aclMockResp := []*m.DashboardAclInfoDTO{
{
DashboardId: 1,
Permission: m.PERMISSION_EDIT,
UserId: 200,
},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
bus.AddHandler("test", func(query *m.GetUserGroupsByUserQuery) error {
query.Result = []*m.UserGroup{}
return nil
})
cmd := m.SaveDashboardCommand{
FolderId: fakeDash.FolderId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": fakeDash.Id,
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
}),
}
Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should be denied access", func() {
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should be denied access", func() {
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Viewer but has an edit permission", func() {
role := m.ROLE_VIEWER
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should be able to get dashboard with edit rights", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is an Org Viewer but has an admin permission", func() {
role := m.ROLE_VIEWER
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should be able to get dashboard with edit rights", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeTrue)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is an Org Editor but has a view permission", func() {
role := m.ROLE_EDITOR
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should not be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
CallGetDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
}
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
dash := dtos.DashboardFullWithMeta{}
err := json.NewDecoder(sc.resp.Body).Decode(&dash)
So(err, ShouldBeNil)
return dash
}
func CallGetDashboardVersion(sc *scenarioContext) {
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{}
return nil
})
sc.handlerFunc = GetDashboardVersion
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func CallGetDashboardVersions(sc *scenarioContext) {
bus.AddHandler("test", func(query *m.GetDashboardVersionsQuery) error {
query.Result = []*m.DashboardVersionDTO{}
return nil
})
sc.handlerFunc = GetDashboardVersions
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func CallDeleteDashboard(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
return nil
})
sc.handlerFunc = DeleteDashboard
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
func CallPostDashboard(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2}
return nil
})
bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return PostDashboard(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@ -56,6 +56,10 @@ func TestDataSourcesProxy(t *testing.T) {
}
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn)
}
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
@ -77,7 +81,7 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = models.ROLE_EDITOR
sc.context.OrgRole = role
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)
}
@ -85,7 +89,12 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
return nil
})
sc.m.Get(url, sc.defaultHandler)
switch method {
case "GET":
sc.m.Get(routePattern, sc.defaultHandler)
case "DELETE":
sc.m.Delete(routePattern, sc.defaultHandler)
}
fn(sc)
})

16
pkg/api/dtos/acl.go Normal file
View File

@ -0,0 +1,16 @@
package dtos
import (
m "github.com/grafana/grafana/pkg/models"
)
type UpdateDashboardAclCommand struct {
Items []DashboardAclUpdateItem `json:"items"`
}
type DashboardAclUpdateItem struct {
UserId int64 `json:"userId"`
UserGroupId int64 `json:"userGroupId"`
Role *m.RoleType `json:"role,omitempty"`
Permission m.PermissionType `json:"permission"`
}

View File

@ -7,20 +7,25 @@ import (
)
type DashboardMeta struct {
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"`
}
type DashboardFullWithMeta struct {

View File

@ -84,16 +84,23 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
data.User.LightTheme = true
}
dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/"},
{Text: "Playlists", Url: setting.AppSubUrl + "/playlists"},
{Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"},
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "New",
Icon: "fa fa-fw fa-plus",
Url: "",
Children: []*dtos.NavLink{
{Text: "Dashboard", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new"},
{Text: "Folder", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"},
{Text: "Import", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"},
},
})
}
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true})
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"})
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"})
dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home"},
{Text: "Playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"},
{Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-snapshot"},
}
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
@ -105,8 +112,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
alertChildNavs := []*dtos.NavLink{
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"},
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"},
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list", Icon: "fa fa-fw fa-list-ul"},
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "fa fa-fw fa-bell-o"},
}
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
@ -122,12 +129,20 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Text: "Data Sources",
Icon: "icon-gf icon-gf-datasources",
Url: setting.AppSubUrl + "/datasources",
Children: []*dtos.NavLink{
{Text: "List", Url: setting.AppSubUrl + "/datasources", Icon: "icon-gf icon-gf-datasources"},
{Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"},
},
})
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Plugins",
Icon: "icon-gf icon-gf-apps",
Url: setting.AppSubUrl + "/plugins",
Children: []*dtos.NavLink{
{Text: "Panels", Url: setting.AppSubUrl + "/plugins?type=panel", Icon: "fa fa-fw fa-stop"},
{Text: "Data sources", Url: setting.AppSubUrl + "/plugins?type=datasource", Icon: "icon-gf icon-gf-datasources"},
{Text: "Apps", Url: setting.AppSubUrl + "/plugins?type=app", Icon: "icon-gf icon-gf-apps"},
},
})
}

View File

@ -130,7 +130,7 @@ func GetPlaylistItems(c *middleware.Context) Response {
func GetPlaylistDashboards(c *middleware.Context) Response {
playlistId := c.ParamsInt64(":id")
playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId)
playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId)
if err != nil {
return ApiError(500, "Could not load dashboards", err)
}

View File

@ -34,18 +34,18 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
return result, nil
}
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
result := make(dtos.PlaylistDashboardsSlice, 0)
if len(dashboardByTag) > 0 {
for _, tag := range dashboardByTag {
searchQuery := search.Query{
Title: "",
Tags: []string{tag},
UserId: userId,
Limit: 100,
IsStarred: false,
OrgId: orgId,
Title: "",
Tags: []string{tag},
SignedInUser: signedInUser,
Limit: 100,
IsStarred: false,
OrgId: orgId,
}
if err := bus.Dispatch(&searchQuery); err == nil {
@ -64,7 +64,7 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb
return result
}
func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := LoadPlaylistItems(playlistId)
dashboardByIds := make([]int64, 0)
@ -89,7 +89,7 @@ func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashb
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
result = append(result, k...)
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...)
result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...)
sort.Sort(result)
return result, nil

View File

@ -17,8 +17,10 @@ func RenderToPng(c *middleware.Context) {
Path: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "60"),
OrgId: c.OrgId,
UserId: c.UserId,
OrgRole: c.OrgRole,
Timezone: queryReader.Get("tz", ""),
}

View File

@ -14,14 +14,16 @@ func Search(c *middleware.Context) {
tags := c.QueryStrings("tag")
starred := c.Query("starred")
limit := c.QueryInt("limit")
dashboardType := c.Query("type")
folderId := c.QueryInt64("folderId")
if limit == 0 {
limit = 1000
}
dbids := make([]int, 0)
dbids := make([]int64, 0)
for _, id := range c.QueryStrings("dashboardIds") {
dashboardId, err := strconv.Atoi(id)
dashboardId, err := strconv.ParseInt(id, 10, 64)
if err == nil {
dbids = append(dbids, dashboardId)
}
@ -30,11 +32,13 @@ func Search(c *middleware.Context) {
searchQuery := search.Query{
Title: query,
Tags: tags,
UserId: c.UserId,
SignedInUser: c.SignedInUser,
Limit: limit,
IsStarred: starred == "true",
OrgId: c.OrgId,
DashboardIds: dbids,
Type: dashboardType,
FolderId: folderId,
}
err := bus.Dispatch(&searchQuery)

View File

@ -219,7 +219,7 @@ func SearchUsers(c *middleware.Context) Response {
return Json(200, query.Result.Users)
}
// GET /api/search
// GET /api/users/search
func SearchUsersWithPaging(c *middleware.Context) Response {
query, err := searchUser(c)
if err != nil {

95
pkg/api/user_group.go Normal file
View File

@ -0,0 +1,95 @@
package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
// POST /api/user-groups
func CreateUserGroup(c *middleware.Context, cmd m.CreateUserGroupCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrUserGroupNameTaken {
return ApiError(409, "User Group name taken", err)
}
return ApiError(500, "Failed to create User Group", err)
}
metrics.M_Api_UserGroup_Create.Inc(1)
return Json(200, &util.DynMap{
"userGroupId": cmd.Result.Id,
"message": "User Group created",
})
}
// PUT /api/user-groups/:userGroupId
func UpdateUserGroup(c *middleware.Context, cmd m.UpdateUserGroupCommand) Response {
cmd.Id = c.ParamsInt64(":userGroupId")
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrUserGroupNameTaken {
return ApiError(400, "User Group name taken", err)
}
return ApiError(500, "Failed to update User Group", err)
}
return ApiSuccess("User Group updated")
}
// DELETE /api/user-groups/:userGroupId
func DeleteUserGroupById(c *middleware.Context) Response {
if err := bus.Dispatch(&m.DeleteUserGroupCommand{Id: c.ParamsInt64(":userGroupId")}); err != nil {
if err == m.ErrUserGroupNotFound {
return ApiError(404, "Failed to delete User Group. ID not found", nil)
}
return ApiError(500, "Failed to update User Group", err)
}
return ApiSuccess("User Group deleted")
}
// GET /api/user-groups/search
func SearchUserGroups(c *middleware.Context) Response {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
query := m.SearchUserGroupsQuery{
Query: c.Query("query"),
Name: c.Query("name"),
Page: page,
Limit: perPage,
OrgId: c.OrgId,
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search User Groups", err)
}
query.Result.Page = page
query.Result.PerPage = perPage
return Json(200, query.Result)
}
// GET /api/user-groups/:userGroupId
func GetUserGroupById(c *middleware.Context) Response {
query := m.GetUserGroupByIdQuery{Id: c.ParamsInt64(":userGroupId")}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrUserGroupNotFound {
return ApiError(404, "User Group not found", err)
}
return ApiError(500, "Failed to get User Group", err)
}
return Json(200, &query.Result)
}

View File

@ -0,0 +1,44 @@
package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
// GET /api/user-groups/:userGroupId/members
func GetUserGroupMembers(c *middleware.Context) Response {
query := m.GetUserGroupMembersQuery{UserGroupId: c.ParamsInt64(":userGroupId")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get User Group Members", err)
}
return Json(200, query.Result)
}
// POST /api/user-groups/:userGroupId/members
func AddUserGroupMember(c *middleware.Context, cmd m.AddUserGroupMemberCommand) Response {
cmd.UserGroupId = c.ParamsInt64(":userGroupId")
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrUserGroupMemberAlreadyAdded {
return ApiError(400, "User is already added to this user group", err)
}
return ApiError(500, "Failed to add Member to User Group", err)
}
return Json(200, &util.DynMap{
"message": "Member added to User Group",
})
}
// DELETE /api/user-groups/:userGroupId/members/:userId
func RemoveUserGroupMember(c *middleware.Context) Response {
if err := bus.Dispatch(&m.RemoveUserGroupMemberCommand{UserGroupId: c.ParamsInt64(":userGroupId"), UserId: c.ParamsInt64(":userId")}); err != nil {
return ApiError(500, "Failed to remove Member from User Group", err)
}
return ApiSuccess("User Group Member removed")
}

View File

@ -0,0 +1,71 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserGroupApiEndpoint(t *testing.T) {
Convey("Given two user groups", t, func() {
mockResult := models.SearchUserGroupQueryResult{
UserGroups: []*models.UserGroup{
{Name: "userGroup1"},
{Name: "userGroup2"},
},
TotalCount: 2,
}
Convey("When searching with no parameters", func() {
loggedInUserScenario("When calling GET on", "/api/user-groups/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUserGroupsQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUserGroups
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
So(len(respJSON.Get("userGroups").MustArray()), ShouldEqual, 2)
})
})
Convey("When searching with page and perpage parameters", func() {
loggedInUserScenario("When calling GET on", "/api/user-groups/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUserGroupsQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUserGroups
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
})
})
}

View File

@ -16,17 +16,21 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type RenderOpts struct {
Path string
Width string
Height string
Timeout string
OrgId int64
Timezone string
Path string
Width string
Height string
Timeout string
OrgId int64
UserId int64
OrgRole models.RoleType
Timezone string
IsAlertContext bool
}
var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
@ -74,7 +78,11 @@ func RenderToPng(params *RenderOpts) (string, error) {
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
pngPath = pngPath + ".png"
renderKey := middleware.AddRenderAuthKey(params.OrgId)
orgRole := params.OrgRole
if params.IsAlertContext {
orgRole = models.ROLE_ADMIN
}
renderKey := middleware.AddRenderAuthKey(params.OrgId, params.UserId, orgRole)
defer middleware.RemoveRenderAuthKey(renderKey)
timeout, err := strconv.Atoi(params.Timeout)

View File

@ -35,6 +35,8 @@ var (
M_Api_Dashboard_Snapshot_Create Counter
M_Api_Dashboard_Snapshot_External Counter
M_Api_Dashboard_Snapshot_Get Counter
M_Api_UserGroup_Create Counter
M_Api_Dashboard_Acl_Update Counter
M_Models_Dashboard_Insert Counter
M_Alerting_Result_State_Alerting Counter
M_Alerting_Result_State_Ok Counter
@ -93,6 +95,9 @@ func initMetricVars(settings *MetricSettings) {
M_Api_User_SignUpCompleted = RegCounter("api.user.signup_completed")
M_Api_User_SignUpInvite = RegCounter("api.user.signup_invite")
M_Api_UserGroup_Create = RegCounter("api.usergroup.create")
M_Api_Dashboard_Acl_Update = RegCounter("api.dashboard.acl.update")
M_Api_Dashboard_Save = RegTimer("api.dashboard.save")
M_Api_Dashboard_Get = RegTimer("api.dashboard.get")
M_Api_Dashboard_Search = RegTimer("api.dashboard.search")

View File

@ -33,14 +33,15 @@ func initContextWithRenderAuth(ctx *Context) bool {
type renderContextFunc func(key string) (string, error)
func AddRenderAuthKey(orgId int64) string {
func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) string {
renderKeysLock.Lock()
key := util.GetRandomString(32)
renderKeys[key] = &m.SignedInUser{
OrgId: orgId,
OrgRole: m.ROLE_VIEWER,
OrgRole: orgRole,
UserId: userId,
}
renderKeysLock.Unlock()

View File

@ -0,0 +1,95 @@
package models
import (
"errors"
"time"
)
type PermissionType int
const (
PERMISSION_VIEW PermissionType = 1 << iota
PERMISSION_EDIT
PERMISSION_ADMIN
)
func (p PermissionType) String() string {
names := map[int]string{
int(PERMISSION_VIEW): "View",
int(PERMISSION_EDIT): "Edit",
int(PERMISSION_ADMIN): "Admin",
}
return names[int(p)]
}
// Typed errors
var (
ErrDashboardAclInfoMissing = errors.New("User id and user group id cannot both be empty for a dashboard permission.")
ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.")
)
// Dashboard ACL model
type DashboardAcl struct {
Id int64
OrgId int64
DashboardId int64
UserId int64
UserGroupId int64
Role *RoleType // pointer to be nullable
Permission PermissionType
Created time.Time
Updated time.Time
}
type DashboardAclInfoDTO struct {
Id int64 `json:"id"`
OrgId int64 `json:"-"`
DashboardId int64 `json:"dashboardId"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UserId int64 `json:"userId"`
UserLogin string `json:"userLogin"`
UserEmail string `json:"userEmail"`
UserGroupId int64 `json:"userGroupId"`
UserGroup string `json:"userGroup"`
Role *RoleType `json:"role,omitempty"`
Permission PermissionType `json:"permission"`
PermissionName string `json:"permissionName"`
}
//
// COMMANDS
//
type UpdateDashboardAclCommand struct {
DashboardId int64
Items []*DashboardAcl
}
type SetDashboardAclCommand struct {
DashboardId int64
OrgId int64
UserId int64
UserGroupId int64
Permission PermissionType
Result DashboardAcl
}
type RemoveDashboardAclCommand struct {
AclId int64
OrgId int64
}
//
// QUERIES
//
type GetDashboardAclInfoListQuery struct {
DashboardId int64
OrgId int64
Result []*DashboardAclInfoDTO
}

View File

@ -0,0 +1,21 @@
package models
import (
"testing"
"fmt"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardAclModel(t *testing.T) {
Convey("When printing a PermissionType", t, func() {
view := PERMISSION_VIEW
printed := fmt.Sprint(view)
Convey("Should output a friendly name", func() {
So(printed, ShouldEqual, "View")
})
})
}

View File

@ -11,11 +11,12 @@ import (
// Typed errors
var (
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
)
type UpdatePluginDashboardError struct {
@ -47,6 +48,9 @@ type Dashboard struct {
UpdatedBy int64
CreatedBy int64
FolderId int64
IsFolder bool
HasAcl bool
Title string
Data *simplejson.Json
@ -111,6 +115,8 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash.UpdatedBy = userId
dash.OrgId = cmd.OrgId
dash.PluginId = cmd.PluginId
dash.IsFolder = cmd.IsFolder
dash.FolderId = cmd.FolderId
dash.UpdateSlug()
return dash
}
@ -138,12 +144,14 @@ type SaveDashboardCommand struct {
OrgId int64 `json:"-"`
RestoredFrom int `json:"-"`
PluginId string `json:"-"`
FolderId int64 `json:"folderId"`
IsFolder bool `json:"isFolder"`
Result *Dashboard
}
type DeleteDashboardCommand struct {
Slug string
Id int64
OrgId int64
}

View File

@ -28,4 +28,27 @@ func TestDashboardModel(t *testing.T) {
})
})
Convey("Given a new dashboard folder", t, func() {
json := simplejson.New()
json.Set("title", "test dash")
cmd := &SaveDashboardCommand{Dashboard: json, IsFolder: true}
dash := cmd.GetDashboardModel()
Convey("Should set IsFolder to true", func() {
So(dash.IsFolder, ShouldBeTrue)
})
})
Convey("Given a child dashboard", t, func() {
json := simplejson.New()
json.Set("title", "test dash")
cmd := &SaveDashboardCommand{Dashboard: json, FolderId: 1}
dash := cmd.GetDashboardModel()
Convey("Should set FolderId", func() {
So(dash.FolderId, ShouldEqual, 1)
})
})
}

View File

@ -32,11 +32,20 @@ func (r RoleType) Includes(other RoleType) bool {
if r == ROLE_ADMIN {
return true
}
if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR {
return other != ROLE_ADMIN
if other == ROLE_READ_ONLY_EDITOR {
return r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
}
return r == other
if other == ROLE_EDITOR {
return r == ROLE_EDITOR
}
if other == ROLE_VIEWER {
return r == ROLE_READ_ONLY_EDITOR || r == ROLE_EDITOR || r == ROLE_VIEWER
}
return false
}
func (r *RoleType) UnmarshalJSON(data []byte) error {

View File

@ -162,6 +162,14 @@ type SignedInUser struct {
HelpFlags1 HelpFlags1
}
func (user *SignedInUser) HasRole(role RoleType) bool {
if user.IsGrafanaAdmin {
return true
}
return user.OrgRole.Includes(role)
}
type UserProfileDTO struct {
Id int64 `json:"id"`
Email string `json:"email"`

68
pkg/models/user_group.go Normal file
View File

@ -0,0 +1,68 @@
package models
import (
"errors"
"time"
)
// Typed errors
var (
ErrUserGroupNotFound = errors.New("User Group not found")
ErrUserGroupNameTaken = errors.New("User Group name is taken")
)
// UserGroup model
type UserGroup struct {
Id int64 `json:"id"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ---------------------
// COMMANDS
type CreateUserGroupCommand struct {
Name string `json:"name" binding:"Required"`
OrgId int64 `json:"-"`
Result UserGroup `json:"-"`
}
type UpdateUserGroupCommand struct {
Id int64
Name string
}
type DeleteUserGroupCommand struct {
Id int64
}
type GetUserGroupByIdQuery struct {
Id int64
Result *UserGroup
}
type GetUserGroupsByUserQuery struct {
UserId int64 `json:"userId"`
Result []*UserGroup `json:"userGroups"`
}
type SearchUserGroupsQuery struct {
Query string
Name string
Limit int
Page int
OrgId int64
Result SearchUserGroupQueryResult
}
type SearchUserGroupQueryResult struct {
TotalCount int64 `json:"totalCount"`
UserGroups []*UserGroup `json:"userGroups"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}

View File

@ -0,0 +1,55 @@
package models
import (
"errors"
"time"
)
// Typed errors
var (
ErrUserGroupMemberAlreadyAdded = errors.New("User is already added to this user group")
)
// UserGroupMember model
type UserGroupMember struct {
Id int64
OrgId int64
UserGroupId int64
UserId int64
Created time.Time
Updated time.Time
}
// ---------------------
// COMMANDS
type AddUserGroupMemberCommand struct {
UserId int64 `json:"userId" binding:"Required"`
OrgId int64 `json:"-"`
UserGroupId int64 `json:"-"`
}
type RemoveUserGroupMemberCommand struct {
UserId int64
UserGroupId int64
}
// ----------------------
// QUERIES
type GetUserGroupMembersQuery struct {
UserGroupId int64
Result []*UserGroupMemberDTO
}
// ----------------------
// Projections and DTOs
type UserGroupMemberDTO struct {
OrgId int64 `json:"orgId"`
UserGroupId int64 `json:"userGroupId"`
UserId int64 `json:"userId"`
Email string `json:"email"`
Login string `json:"login"`
}

View File

@ -15,6 +15,7 @@ type PluginDashboardInfoDTO struct {
Imported bool `json:"imported"`
ImportedUri string `json:"importedUri"`
Slug string `json:"slug"`
DashboardId int64 `json:"dashboardId"`
ImportedRevision int64 `json:"importedRevision"`
Revision int64 `json:"revision"`
Description string `json:"description"`
@ -60,6 +61,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
// find existing dashboard
for _, existingDash := range query.Result {
if existingDash.Slug == dashboard.Slug {
res.DashboardId = existingDash.Id
res.Imported = true
res.ImportedUri = "db/" + existingDash.Slug
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
@ -74,8 +76,9 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
for _, dash := range query.Result {
if _, exists := existingMatches[dash.Id]; !exists {
result = append(result, &PluginDashboardInfoDTO{
Slug: dash.Slug,
Removed: true,
Slug: dash.Slug,
DashboardId: dash.Id,
Removed: true,
})
}
}

View File

@ -75,7 +75,7 @@ func syncPluginDashboards(pluginDef *PluginBase, orgId int64) {
if dash.Removed {
plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Slug: dash.Slug}
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Id: dash.DashboardId}
if err := bus.Dispatch(&deleteCmd); err != nil {
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
return
@ -124,7 +124,7 @@ func handlePluginStateChanged(event *m.PluginStateChangedEvent) error {
return err
} else {
for _, dash := range query.Result {
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Slug: dash.Slug}
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)

View File

@ -79,10 +79,11 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
}
renderOpts := &renderer.RenderOpts{
Width: "800",
Height: "400",
Timeout: "30",
OrgId: context.Rule.OrgId,
Width: "800",
Height: "400",
Timeout: "30",
OrgId: context.Rule.OrgId,
IsAlertContext: true,
}
if slug, err := context.GetDashboardSlug(); err != nil {

View File

@ -0,0 +1,127 @@
package guardian
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
type DashboardGuardian struct {
user *m.SignedInUser
dashId int64
orgId int64
acl []*m.DashboardAclInfoDTO
groups []*m.UserGroup
log log.Logger
}
func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *DashboardGuardian {
return &DashboardGuardian{
user: user,
dashId: dashId,
orgId: orgId,
log: log.New("guardians.dashboard"),
}
}
func (g *DashboardGuardian) CanSave() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanEdit() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanView() (bool, error) {
return g.HasPermission(m.PERMISSION_VIEW)
}
func (g *DashboardGuardian) CanAdmin() (bool, error) {
return g.HasPermission(m.PERMISSION_ADMIN)
}
func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
}
acl, err := g.GetAcl()
if err != nil {
return false, err
}
orgRole := g.user.OrgRole
if orgRole == m.ROLE_READ_ONLY_EDITOR {
orgRole = m.ROLE_VIEWER
}
userGroupAclItems := []*m.DashboardAclInfoDTO{}
for _, p := range acl {
// user match
if p.UserId == g.user.UserId && p.Permission >= permission {
return true, nil
}
// role match
if p.Role != nil {
if *p.Role == orgRole && p.Permission >= permission {
return true, nil
}
}
// remember this rule for later
if p.UserGroupId > 0 {
userGroupAclItems = append(userGroupAclItems, p)
}
}
// do we have group rules?
if len(userGroupAclItems) == 0 {
return false, nil
}
// load groups
userGroups, err := g.getUserGroups()
if err != nil {
return false, err
}
// evalute group rules
for _, p := range acl {
for _, ug := range userGroups {
if ug.Id == p.UserGroupId && p.Permission >= permission {
return true, nil
}
}
}
return false, nil
}
// Returns dashboard acl
func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
if g.acl != nil {
return g.acl, nil
}
query := m.GetDashboardAclInfoListQuery{DashboardId: g.dashId, OrgId: g.orgId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
g.acl = query.Result
return g.acl, nil
}
func (g *DashboardGuardian) getUserGroups() ([]*m.UserGroup, error) {
if g.groups != nil {
return g.groups, nil
}
query := m.GetUserGroupsByUserQuery{UserId: g.user.UserId}
err := bus.Dispatch(&query)
g.groups = query.Result
return query.Result, err
}

View File

@ -1,77 +1,35 @@
package search
import (
"log"
"path/filepath"
"sort"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
var jsonDashIndex *JsonDashIndex
func Init() {
bus.AddHandler("search", searchHandler)
jsonIndexCfg, _ := setting.Cfg.GetSection("dashboards.json")
if jsonIndexCfg == nil {
log.Fatal("Config section missing: dashboards.json")
return
}
jsonIndexEnabled := jsonIndexCfg.Key("enabled").MustBool(false)
if jsonIndexEnabled {
jsonFilesPath := jsonIndexCfg.Key("path").String()
if !filepath.IsAbs(jsonFilesPath) {
jsonFilesPath = filepath.Join(setting.HomePath, jsonFilesPath)
}
jsonDashIndex = NewJsonDashIndex(jsonFilesPath)
go jsonDashIndex.updateLoop()
}
}
func searchHandler(query *Query) error {
hits := make(HitList, 0)
dashQuery := FindPersistedDashboardsQuery{
Title: query.Title,
UserId: query.UserId,
SignedInUser: query.SignedInUser,
IsStarred: query.IsStarred,
OrgId: query.OrgId,
DashboardIds: query.DashboardIds,
Type: query.Type,
FolderId: query.FolderId,
Tags: query.Tags,
Limit: query.Limit,
}
if err := bus.Dispatch(&dashQuery); err != nil {
return err
}
hits := make(HitList, 0)
hits = append(hits, dashQuery.Result...)
if jsonDashIndex != nil {
jsonHits, err := jsonDashIndex.Search(query)
if err != nil {
return err
}
hits = append(hits, jsonHits...)
}
// filter out results with tag filter
if len(query.Tags) > 0 {
filtered := HitList{}
for _, hit := range hits {
if hasRequiredTags(query.Tags, hit.Tags) {
filtered = append(filtered, hit)
}
}
hits = filtered
}
// sort main result array
sort.Sort(hits)
@ -85,7 +43,7 @@ func searchHandler(query *Query) error {
}
// add isStarred info
if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil {
if err := setIsStarredFlagOnSearchResults(query.SignedInUser.UserId, hits); err != nil {
return err
}
@ -93,25 +51,6 @@ func searchHandler(query *Query) error {
return nil
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
func hasRequiredTags(queryTags, hitTags []string) bool {
for _, queryTag := range queryTags {
if !stringInSlice(queryTag, hitTags) {
return false
}
}
return true
}
func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
query := m.GetUserStarsQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
@ -126,10 +65,3 @@ func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
return nil
}
func GetDashboardFromJsonIndex(filename string) *m.Dashboard {
if jsonDashIndex == nil {
return nil
}
return jsonDashIndex.GetDashboard(filename)
}

View File

@ -11,14 +11,14 @@ import (
func TestSearch(t *testing.T) {
Convey("Given search query", t, func() {
jsonDashIndex = NewJsonDashIndex("../../../public/dashboards/")
query := Query{Limit: 2000}
query := Query{Limit: 2000, SignedInUser: &m.SignedInUser{IsGrafanaAdmin: true}}
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
query.Result = HitList{
&Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}},
&Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}},
&Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}},
&Hit{Id: 16, Title: "CCAA", Type: "dash-db", Tags: []string{"BB", "AA"}},
&Hit{Id: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}},
&Hit{Id: 15, Title: "BBAA", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
&Hit{Id: 25, Title: "bbAAa", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
&Hit{Id: 17, Title: "FOLDER", Type: "dash-folder"},
}
return nil
})
@ -28,34 +28,29 @@ func TestSearch(t *testing.T) {
return nil
})
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{IsGrafanaAdmin: true}
return nil
})
Convey("That is empty", func() {
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return sorted results", func() {
So(query.Result[0].Title, ShouldEqual, "AABB")
So(query.Result[1].Title, ShouldEqual, "BBAA")
So(query.Result[2].Title, ShouldEqual, "CCAA")
So(query.Result[0].Title, ShouldEqual, "FOLDER")
So(query.Result[1].Title, ShouldEqual, "AABB")
So(query.Result[2].Title, ShouldEqual, "BBAA")
So(query.Result[3].Title, ShouldEqual, "bbAAa")
So(query.Result[4].Title, ShouldEqual, "CCAA")
})
Convey("should return sorted tags", func() {
So(query.Result[1].Tags[0], ShouldEqual, "AA")
So(query.Result[1].Tags[1], ShouldEqual, "BB")
So(query.Result[1].Tags[2], ShouldEqual, "EE")
So(query.Result[3].Tags[0], ShouldEqual, "AA")
So(query.Result[3].Tags[1], ShouldEqual, "BB")
So(query.Result[3].Tags[2], ShouldEqual, "EE")
})
})
Convey("That filters by tag", func() {
query.Tags = []string{"BB", "AA"}
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return correct results", func() {
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Title, ShouldEqual, "BBAA")
So(query.Result[1].Title, ShouldEqual, "CCAA")
})
})
})
}

View File

@ -1,137 +0,0 @@
package search
import (
"os"
"path/filepath"
"strings"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
type JsonDashIndex struct {
path string
items []*JsonDashIndexItem
}
type JsonDashIndexItem struct {
TitleLower string
TagsCsv string
Path string
Dashboard *m.Dashboard
}
func NewJsonDashIndex(path string) *JsonDashIndex {
log.Info("Creating json dashboard index for path: %v", path)
index := JsonDashIndex{}
index.path = path
index.updateIndex()
return &index
}
func (index *JsonDashIndex) updateLoop() {
ticker := time.NewTicker(time.Minute)
for {
select {
case <-ticker.C:
if err := index.updateIndex(); err != nil {
log.Error(3, "Failed to update dashboard json index %v", err)
}
}
}
}
func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) {
results := make([]*Hit, 0)
if query.IsStarred {
return results, nil
}
queryStr := strings.ToLower(query.Title)
for _, item := range index.items {
if len(results) > query.Limit {
break
}
// add results with matchig title filter
if strings.Contains(item.TitleLower, queryStr) {
results = append(results, &Hit{
Type: DashHitJson,
Title: item.Dashboard.Title,
Tags: item.Dashboard.GetTags(),
Uri: "file/" + item.Path,
})
}
}
return results, nil
}
func (index *JsonDashIndex) GetDashboard(path string) *m.Dashboard {
for _, item := range index.items {
if item.Path == path {
return item.Dashboard
}
}
return nil
}
func (index *JsonDashIndex) updateIndex() error {
var items = make([]*JsonDashIndexItem, 0)
visitor := func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if f.IsDir() {
return nil
}
if strings.HasSuffix(f.Name(), ".json") {
dash, err := loadDashboardFromFile(path)
if err != nil {
return err
}
items = append(items, dash)
}
return nil
}
if err := filepath.Walk(index.path, visitor); err != nil {
return err
}
index.items = items
return nil
}
func loadDashboardFromFile(filename string) (*JsonDashIndexItem, error) {
reader, err := os.Open(filename)
if err != nil {
return nil, err
}
defer reader.Close()
data, err := simplejson.NewFromReader(reader)
if err != nil {
return nil, err
}
stat, _ := os.Stat(filename)
item := &JsonDashIndexItem{}
item.Dashboard = m.NewDashboardFromJson(data)
item.TitleLower = strings.ToLower(item.Dashboard.Title)
item.TagsCsv = strings.Join(item.Dashboard.GetTags(), ",")
item.Path = stat.Name()
return item, nil
}

View File

@ -1,42 +0,0 @@
package search
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestJsonDashIndex(t *testing.T) {
Convey("Given the json dash index", t, func() {
index := NewJsonDashIndex("../../../public/dashboards/")
Convey("Should be able to update index", func() {
err := index.updateIndex()
So(err, ShouldBeNil)
})
Convey("Should be able to search index", func() {
res, err := index.Search(&Query{Title: "", Limit: 20})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 3)
})
Convey("Should be able to search index by title", func() {
res, err := index.Search(&Query{Title: "home", Limit: 20})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 1)
So(res[0].Title, ShouldEqual, "Home")
})
Convey("Should not return when starred is filtered", func() {
res, err := index.Search(&Query{Title: "", IsStarred: true})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 0)
})
})
}

View File

@ -1,37 +1,54 @@
package search
import "strings"
import "github.com/grafana/grafana/pkg/models"
type HitType string
const (
DashHitDB HitType = "dash-db"
DashHitHome HitType = "dash-home"
DashHitJson HitType = "dash-json"
DashHitScripted HitType = "dash-scripted"
DashHitDB HitType = "dash-db"
DashHitHome HitType = "dash-home"
DashHitFolder HitType = "dash-folder"
)
type Hit struct {
Id int64 `json:"id"`
Title string `json:"title"`
Uri string `json:"uri"`
Type HitType `json:"type"`
Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"`
Id int64 `json:"id"`
Title string `json:"title"`
Uri string `json:"uri"`
Type HitType `json:"type"`
Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"`
FolderId int64 `json:"folderId,omitempty"`
FolderTitle string `json:"folderTitle,omitempty"`
FolderSlug string `json:"folderSlug,omitempty"`
}
type HitList []*Hit
func (s HitList) Len() int { return len(s) }
func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title }
func (s HitList) Len() int { return len(s) }
func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s HitList) Less(i, j int) bool {
if s[i].Type == "dash-folder" && s[j].Type == "dash-db" {
return true
}
if s[i].Type == "dash-db" && s[j].Type == "dash-folder" {
return false
}
return strings.ToLower(s[i].Title) < strings.ToLower(s[j].Title)
}
type Query struct {
Title string
Tags []string
OrgId int64
UserId int64
SignedInUser *models.SignedInUser
Limit int
IsStarred bool
DashboardIds []int
Type string
DashboardIds []int64
FolderId int64
Result HitList
}
@ -39,9 +56,13 @@ type Query struct {
type FindPersistedDashboardsQuery struct {
Title string
OrgId int64
UserId int64
SignedInUser *models.SignedInUser
IsStarred bool
DashboardIds []int
DashboardIds []int64
Type string
FolderId int64
Tags []string
Limit int
Result HitList
}

View File

@ -12,7 +12,7 @@ func TestAlertingDataAccess(t *testing.T) {
Convey("Testing Alerting data access", t, func() {
InitTestDB(t)
testDash := insertTestDashboard("dashboard with alerts", 1, "alert")
testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
items := []*m.Alert{
{
@ -192,7 +192,7 @@ func TestAlertingDataAccess(t *testing.T) {
err = DeleteDashboard(&m.DeleteDashboardCommand{
OrgId: 1,
Slug: testDash.Slug,
Id: testDash.Id,
})
So(err, ShouldBeNil)

View File

@ -3,6 +3,7 @@ package sqlstore
import (
"bytes"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
@ -70,6 +71,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
}
}
err = setHasAcl(sess, dash)
if err != nil {
return err
}
parentVersion := dash.Version
affectedRows := int64(0)
@ -79,9 +85,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
dash.Data.Set("version", dash.Version)
affectedRows, err = sess.Insert(dash)
} else {
dash.Version += 1
dash.Version++
dash.Data.Set("version", dash.Version)
affectedRows, err = sess.Id(dash.Id).Update(dash)
affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash)
}
if err != nil {
@ -110,7 +116,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
return m.ErrDashboardNotFound
}
// delete existing tabs
// delete existing tags
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
if err != nil {
return err
@ -125,13 +131,37 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
}
}
}
cmd.Result = dash
return err
})
}
func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
// check if parent has acl
if dash.FolderId > 0 {
var parent m.Dashboard
if hasParent, err := sess.Where("folder_id=?", dash.FolderId).Get(&parent); err != nil {
return err
} else if hasParent && parent.HasAcl {
dash.HasAcl = true
}
}
// check if dash has its own acl
if dash.Id > 0 {
if res, err := sess.Query("SELECT 1 from dashboard_acl WHERE dashboard_id =?", dash.Id); err != nil {
return err
} else {
if len(res) > 0 {
dash.HasAcl = true
}
}
}
return nil
}
func GetDashboard(query *m.GetDashboardQuery) error {
dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id}
has, err := x.Get(&dashboard)
@ -148,48 +178,94 @@ func GetDashboard(query *m.GetDashboardQuery) error {
}
type DashboardSearchProjection struct {
Id int64
Title string
Slug string
Term string
Id int64
Title string
Slug string
Term string
IsFolder bool
FolderId int64
FolderSlug string
FolderTitle string
}
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
limit := query.Limit
if limit == 0 {
limit = 1000
}
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`SELECT
dashboard.id,
dashboard.title,
dashboard.slug,
dashboard_tag.term
FROM dashboard
LEFT OUTER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id`)
sql.WriteString(`
SELECT
dashboard.id,
dashboard.title,
dashboard.slug,
dashboard_tag.term,
dashboard.is_folder,
dashboard.folder_id,
folder.slug as folder_slug,
folder.title as folder_title
FROM (
SELECT
dashboard.id FROM dashboard
LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id
`)
// add tags filter
if len(query.Tags) > 0 {
sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(query.Tags)-1) + `)`)
for _, tag := range query.Tags {
params = append(params, tag)
}
}
// this ends the inner select (tag filtered part)
sql.WriteString(`
GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
ORDER BY dashboard.title ASC LIMIT ?) as ids`)
params = append(params, len(query.Tags))
params = append(params, limit)
sql.WriteString(`
INNER JOIN dashboard on ids.id = dashboard.id
LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id
LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`)
if query.IsStarred {
sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
}
sql.WriteString(` WHERE dashboard.org_id=?`)
params = append(params, query.OrgId)
params = append(params, query.SignedInUser.OrgId)
if query.IsStarred {
sql.WriteString(` AND star.user_id=?`)
params = append(params, query.UserId)
params = append(params, query.SignedInUser.UserId)
}
if len(query.DashboardIds) > 0 {
sql.WriteString(" AND (")
for i, dashboardId := range query.DashboardIds {
if i != 0 {
sql.WriteString(" OR")
}
sql.WriteString(" dashboard.id = ?")
sql.WriteString(` AND dashboard.id IN (?` + strings.Repeat(",?", len(query.DashboardIds)-1) + `)`)
for _, dashboardId := range query.DashboardIds {
params = append(params, dashboardId)
}
sql.WriteString(")")
}
if query.SignedInUser.OrgRole != m.ROLE_ADMIN {
allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in (
SELECT distinct d.id AS DashboardId
FROM dashboard AS d
LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
LEFT JOIN user_group_member as ugm on ugm.user_group_id = da.user_group_id
LEFT JOIN org_user ou on ou.role = da.role
WHERE
d.has_acl = 1 and
(da.user_id = ? or ugm.user_id = ? or ou.id is not null)
and d.org_id = ?
))`
sql.WriteString(allowedDashboardsSubQuery)
params = append(params, query.SignedInUser.UserId, query.SignedInUser.UserId, query.SignedInUser.OrgId)
}
if len(query.Title) > 0 {
@ -197,15 +273,54 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
params = append(params, "%"+query.Title+"%")
}
if len(query.Type) > 0 && query.Type == "dash-folder" {
sql.WriteString(" AND dashboard.is_folder = 1")
}
if len(query.Type) > 0 && query.Type == "dash-db" {
sql.WriteString(" AND dashboard.is_folder = 0")
}
if query.FolderId > 0 {
sql.WriteString(" AND dashboard.folder_id = ?")
params = append(params, query.FolderId)
}
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
var res []DashboardSearchProjection
err := x.Sql(sql.String(), params...).Find(&res)
if err != nil {
return nil, err
}
return res, nil
}
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
res, err := findDashboards(query)
if err != nil {
return err
}
makeQueryResult(query, res)
return nil
}
func getHitType(item DashboardSearchProjection) search.HitType {
var hitType search.HitType
if item.IsFolder {
hitType = search.DashHitFolder
} else {
hitType = search.DashHitDB
}
return hitType
}
func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []DashboardSearchProjection) {
query.Result = make([]*search.Hit, 0)
hits := make(map[int64]*search.Hit)
@ -213,11 +328,14 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
hit, exists := hits[item.Id]
if !exists {
hit = &search.Hit{
Id: item.Id,
Title: item.Title,
Uri: "db/" + item.Slug,
Type: search.DashHitDB,
Tags: []string{},
Id: item.Id,
Title: item.Title,
Uri: "db/" + item.Slug,
Type: getHitType(item),
FolderId: item.FolderId,
FolderTitle: item.FolderTitle,
FolderSlug: item.FolderSlug,
Tags: []string{},
}
query.Result = append(query.Result, hit)
hits[item.Id] = hit
@ -226,8 +344,6 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
hit.Tags = append(hit.Tags, item.Term)
}
}
return err
}
func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
@ -247,7 +363,7 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
return inTransaction(func(sess *DBSession) error {
dashboard := m.Dashboard{Slug: cmd.Slug, OrgId: cmd.OrgId}
dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
has, err := sess.Get(&dashboard)
if err != nil {
return err
@ -261,6 +377,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
"DELETE FROM dashboard WHERE id = ?",
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
"DELETE FROM dashboard WHERE folder_id = ?",
}
for _, sql := range deletes {

View File

@ -0,0 +1,184 @@
package sqlstore
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
func init() {
bus.AddHandler("sql", SetDashboardAcl)
bus.AddHandler("sql", UpdateDashboardAcl)
bus.AddHandler("sql", RemoveDashboardAcl)
bus.AddHandler("sql", GetDashboardAclInfoList)
}
func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
// delete existing items
_, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", cmd.DashboardId)
if err != nil {
return err
}
for _, item := range cmd.Items {
if item.UserId == 0 && item.UserGroupId == 0 && !item.Role.IsValid() {
return m.ErrDashboardAclInfoMissing
}
if item.DashboardId == 0 {
return m.ErrDashboardPermissionDashboardEmpty
}
sess.Nullable("user_id", "user_group_id")
if _, err := sess.Insert(item); err != nil {
return err
}
}
// Update dashboard HasAcl flag
dashboard := m.Dashboard{HasAcl: true}
if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
return err
}
return nil
})
}
func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
if cmd.UserId == 0 && cmd.UserGroupId == 0 {
return m.ErrDashboardAclInfoMissing
}
if cmd.DashboardId == 0 {
return m.ErrDashboardPermissionDashboardEmpty
}
if res, err := sess.Query("SELECT 1 from "+dialect.Quote("dashboard_acl")+" WHERE dashboard_id =? and (user_group_id=? or user_id=?)", cmd.DashboardId, cmd.UserGroupId, cmd.UserId); err != nil {
return err
} else if len(res) == 1 {
entity := m.DashboardAcl{
Permission: cmd.Permission,
Updated: time.Now(),
}
if _, err := sess.Cols("updated", "permission").Where("dashboard_id =? and (user_group_id=? or user_id=?)", cmd.DashboardId, cmd.UserGroupId, cmd.UserId).Update(&entity); err != nil {
return err
}
return nil
}
entity := m.DashboardAcl{
OrgId: cmd.OrgId,
UserGroupId: cmd.UserGroupId,
UserId: cmd.UserId,
Created: time.Now(),
Updated: time.Now(),
DashboardId: cmd.DashboardId,
Permission: cmd.Permission,
}
cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"}
if cmd.UserId != 0 {
cols = append(cols, "user_id")
}
if cmd.UserGroupId != 0 {
cols = append(cols, "user_group_id")
}
_, err := sess.Cols(cols...).Insert(&entity)
if err != nil {
return err
}
cmd.Result = entity
// Update dashboard HasAcl flag
dashboard := m.Dashboard{
HasAcl: true,
}
if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
return err
}
return nil
})
}
func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error {
return inTransaction(func(sess *DBSession) error {
var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?"
_, err := sess.Exec(rawSQL, cmd.OrgId, cmd.AclId)
if err != nil {
return err
}
return err
})
}
func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
dashboardFilter := fmt.Sprintf(`IN (
SELECT %d
UNION
SELECT folder_id from dashboard where id = %d
)`, query.DashboardId, query.DashboardId)
rawSQL := `
SELECT
da.id,
da.org_id,
da.dashboard_id,
da.user_id,
da.user_group_id,
da.permission,
da.role,
da.created,
da.updated,
u.login AS user_login,
u.email AS user_email,
ug.name AS user_group
FROM` + dialect.Quote("dashboard_acl") + ` as da
LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
LEFT OUTER JOIN user_group ug on ug.id = da.user_group_id
WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ?
-- Also include default permission if has_acl = 0
UNION
SELECT
da.id,
da.org_id,
da.dashboard_id,
da.user_id,
da.user_group_id,
da.permission,
da.role,
da.created,
da.updated,
'' as user_login,
'' as user_email,
'' as user_group
FROM dashboard_acl as da,
dashboard as dash
LEFT JOIN dashboard folder on dash.folder_id = folder.id
WHERE dash.id = ? AND (dash.has_acl = 0 or folder.has_acl = 0) AND da.dashboard_id = -1
`
query.Result = make([]*m.DashboardAclInfoDTO, 0)
err := x.SQL(rawSQL, query.OrgId, query.DashboardId).Find(&query.Result)
for _, p := range query.Result {
p.PermissionName = p.Permission.String()
}
return err
}

View File

@ -0,0 +1,236 @@
package sqlstore
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestDashboardAclDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
Convey("Given a dashboard folder and a user", func() {
currentUser := createUser("viewer", "Viewer", false)
savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp")
Convey("When adding dashboard permission with userId and userGroupId set to 0", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldEqual, m.ErrDashboardAclInfoMissing)
})
Convey("Given dashboard folder with default permissions", func() {
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
})
})
Convey("Given dashboard folder permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
})
Convey("Given child dashboard permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: childDash.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include acl for parent folder and child", func() {
query := m.GetDashboardAclInfoListQuery{OrgId: 1, DashboardId: childDash.Id}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(query.Result[1].DashboardId, ShouldEqual, childDash.Id)
})
})
})
Convey("Given child dashboard permission in folder with no permissions", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: childDash.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include default acl for parent folder and the child acl", func() {
query := m.GetDashboardAclInfoListQuery{OrgId: 1, DashboardId: childDash.Id}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
defaultPermissionsId := -1
So(len(query.Result), ShouldEqual, 3)
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[2].DashboardId, ShouldEqual, childDash.Id)
})
})
Convey("Should be able to add dashboard permission", func() {
setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
}
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil)
So(setDashAclCmd.Result.Id, ShouldEqual, 3)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
So(q1.Result[0].PermissionName, ShouldEqual, "Edit")
So(q1.Result[0].UserId, ShouldEqual, currentUser.Id)
So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login)
So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email)
So(q1.Result[0].Id, ShouldEqual, setDashAclCmd.Result.Id)
Convey("Should update hasAcl field to true for dashboard folder and its children", func() {
q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}}
err := GetDashboards(q2)
So(err, ShouldBeNil)
So(q2.Result[0].HasAcl, ShouldBeTrue)
So(q2.Result[1].HasAcl, ShouldBeTrue)
})
Convey("Should be able to update an existing permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserId: 1,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].UserId, ShouldEqual, 1)
})
Convey("Should be able to delete an existing permission", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1,
AclId: setDashAclCmd.Result.Id,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
})
Convey("Given a user group", func() {
group1 := m.CreateUserGroupCommand{Name: "group1 name", OrgId: 1}
err := CreateUserGroup(&group1)
So(err, ShouldBeNil)
Convey("Should be able to add a user permission for a user group", func() {
setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1,
UserGroupId: group1.Result.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
}
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
So(q1.Result[0].UserGroupId, ShouldEqual, group1.Result.Id)
Convey("Should be able to delete an existing permission for a user group", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1,
AclId: setDashAclCmd.Result.Id,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
})
Convey("Should be able to update an existing permission for a user group", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
UserGroupId: group1.Result.Id,
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].UserGroupId, ShouldEqual, group1.Result.Id)
})
})
})
})
}

View File

@ -5,42 +5,35 @@ import (
. "github.com/smartystreets/goconvey/convey"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/setting"
)
func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard {
cmd := m.SaveDashboardCommand{
OrgId: orgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
return cmd.Result
}
func TestDashboardDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
Convey("Given saved dashboard", func() {
savedDash := insertTestDashboard("test dash 23", 1, "prod", "webapp")
insertTestDashboard("test dash 45", 1, "prod")
insertTestDashboard("test dash 67", 1, "prod", "webapp")
savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
savedDash := insertTestDashboard("test dash 23", 1, savedFolder.Id, false, "prod", "webapp")
insertTestDashboard("test dash 45", 1, savedFolder.Id, false, "prod")
insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp")
Convey("Should return dashboard model", func() {
So(savedDash.Title, ShouldEqual, "test dash 23")
So(savedDash.Slug, ShouldEqual, "test-dash-23")
So(savedDash.Id, ShouldNotEqual, 0)
So(savedDash.IsFolder, ShouldBeFalse)
So(savedDash.FolderId, ShouldBeGreaterThan, 0)
So(savedFolder.Title, ShouldEqual, "1 test dash folder")
So(savedFolder.Slug, ShouldEqual, "1-test-dash-folder")
So(savedFolder.Id, ShouldNotEqual, 0)
So(savedFolder.IsFolder, ShouldBeTrue)
So(savedFolder.FolderId, ShouldEqual, 0)
})
Convey("Should be able to get dashboard", func() {
@ -54,15 +47,14 @@ func TestDashboardDataAccess(t *testing.T) {
So(query.Result.Title, ShouldEqual, "test dash 23")
So(query.Result.Slug, ShouldEqual, "test-dash-23")
So(query.Result.IsFolder, ShouldBeFalse)
})
Convey("Should be able to delete dashboard", func() {
insertTestDashboard("delete me", 1, "delete this")
dashboardSlug := slug.Make("delete me")
dash := insertTestDashboard("delete me", 1, 0, false, "delete this")
err := DeleteDashboard(&m.DeleteDashboardCommand{
Slug: dashboardSlug,
Id: dash.Id,
OrgId: 1,
})
@ -102,10 +94,11 @@ func TestDashboardDataAccess(t *testing.T) {
So(err, ShouldNotBeNil)
})
Convey("Should be able to search for dashboard", func() {
Convey("Should be able to search for dashboard folder", func() {
query := search.FindPersistedDashboardsQuery{
Title: "test dash 23",
OrgId: 1,
Title: "1 test dash folder",
OrgId: 1,
SignedInUser: &m.SignedInUser{OrgId: 1},
}
err := SearchDashboards(&query)
@ -113,14 +106,29 @@ func TestDashboardDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 1)
hit := query.Result[0]
So(len(hit.Tags), ShouldEqual, 2)
So(hit.Type, ShouldEqual, search.DashHitFolder)
})
Convey("Should be able to search for a dashboard folder's children", func() {
query := search.FindPersistedDashboardsQuery{
OrgId: 1,
FolderId: savedFolder.Id,
SignedInUser: &m.SignedInUser{OrgId: 1},
}
err := SearchDashboards(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
hit := query.Result[0]
So(hit.Id, ShouldEqual, savedDash.Id)
})
Convey("Should be able to search for dashboard by dashboard ids", func() {
Convey("should be able to find two dashboards by id", func() {
query := search.FindPersistedDashboardsQuery{
DashboardIds: []int{1, 2},
OrgId: 1,
DashboardIds: []int64{2, 3},
SignedInUser: &m.SignedInUser{OrgId: 1},
}
err := SearchDashboards(&query)
@ -137,8 +145,8 @@ func TestDashboardDataAccess(t *testing.T) {
Convey("DashboardIds that does not exists should not cause errors", func() {
query := search.FindPersistedDashboardsQuery{
DashboardIds: []int{1000},
OrgId: 1,
DashboardIds: []int64{1000},
SignedInUser: &m.SignedInUser{OrgId: 1},
}
err := SearchDashboards(&query)
@ -161,6 +169,63 @@ func TestDashboardDataAccess(t *testing.T) {
So(err, ShouldNotBeNil)
})
Convey("Should be able to update dashboard and remove folderId", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": 1,
"title": "folderId",
"tags": []interface{}{},
}),
Overwrite: true,
FolderId: 2,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.FolderId, ShouldEqual, 2)
cmd = m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": 1,
"title": "folderId",
"tags": []interface{}{},
}),
FolderId: 0,
Overwrite: true,
}
err = SaveDashboard(&cmd)
So(err, ShouldBeNil)
query := m.GetDashboardQuery{
Slug: cmd.Result.Slug,
OrgId: 1,
}
err = GetDashboard(&query)
So(err, ShouldBeNil)
So(query.Result.FolderId, ShouldEqual, 0)
})
Convey("Should be able to delete a dashboard folder and its children", func() {
deleteCmd := &m.DeleteDashboardCommand{Id: savedFolder.Id}
err := DeleteDashboard(deleteCmd)
So(err, ShouldBeNil)
query := search.FindPersistedDashboardsQuery{
OrgId: 1,
FolderId: savedFolder.Id,
SignedInUser: &m.SignedInUser{},
}
err = SearchDashboards(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 0)
})
Convey("Should be able to get dashboard tags", func() {
query := m.GetDashboardTagsQuery{OrgId: 1}
@ -171,7 +236,7 @@ func TestDashboardDataAccess(t *testing.T) {
})
Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() {
starredDash := insertTestDashboard("starred dash", 1)
starredDash := insertTestDashboard("starred dash", 1, 0, false)
StarDashboard(&m.StarDashboardCommand{
DashboardId: starredDash.Id,
UserId: 10,
@ -183,7 +248,7 @@ func TestDashboardDataAccess(t *testing.T) {
})
Convey("Should be able to search for starred dashboards", func() {
query := search.FindPersistedDashboardsQuery{OrgId: 1, UserId: 10, IsStarred: true}
query := search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: 10, OrgId: 1}, IsStarred: true}
err := SearchDashboards(&query)
So(err, ShouldBeNil)
@ -192,5 +257,256 @@ func TestDashboardDataAccess(t *testing.T) {
})
})
})
Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() {
folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp")
childDash := insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp")
insertTestDashboard("test dash 45", 1, folder.Id, false, "prod")
currentUser := createUser("viewer", "Viewer", false)
Convey("and no acls are set", func() {
Convey("should return all dashboards", func() {
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Id, ShouldEqual, folder.Id)
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
})
})
Convey("and acl is set for dashboard folder", func() {
var otherUser int64 = 999
updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
Convey("should not return folder", func() {
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
})
Convey("when the user is given permission", func() {
updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT)
Convey("should be able to access folder", func() {
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Id, ShouldEqual, folder.Id)
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
})
})
Convey("when the user is an admin", func() {
Convey("should be able to access folder", func() {
query := &search.FindPersistedDashboardsQuery{
SignedInUser: &m.SignedInUser{
UserId: currentUser.Id,
OrgId: 1,
OrgRole: m.ROLE_ADMIN,
},
OrgId: 1,
DashboardIds: []int64{folder.Id, dashInRoot.Id},
}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Id, ShouldEqual, folder.Id)
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
})
})
})
Convey("and acl is set for dashboard child and folder has all permissions removed", func() {
var otherUser int64 = 999
aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
removeAcl(aclId)
updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT)
Convey("should not return folder or child", func() {
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
})
Convey("when the user is given permission to child", func() {
updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT)
Convey("should be able to search for child dashboard but not folder", func() {
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Id, ShouldEqual, childDash.Id)
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
})
})
Convey("when the user is an admin", func() {
Convey("should be able to search for child dash and folder", func() {
query := &search.FindPersistedDashboardsQuery{
SignedInUser: &m.SignedInUser{
UserId: currentUser.Id,
OrgId: 1,
OrgRole: m.ROLE_ADMIN,
},
OrgId: 1,
DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id},
}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 3)
So(query.Result[0].Id, ShouldEqual, folder.Id)
So(query.Result[1].Id, ShouldEqual, childDash.Id)
So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
})
})
})
})
Convey("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func() {
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod")
childDash1 := insertTestDashboard("child dash 1", 1, folder1.Id, false, "prod")
childDash2 := insertTestDashboard("child dash 2", 1, folder2.Id, false, "prod")
currentUser := createUser("viewer", "Viewer", false)
Convey("and acl is set for one dashboard folder", func() {
var otherUser int64 = 999
updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT)
Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() {
movedDash := moveDashboard(1, childDash2.Data, folder1.Id)
So(movedDash.HasAcl, ShouldBeTrue)
Convey("should not return folder with acl or its children", func() {
query := &search.FindPersistedDashboardsQuery{
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
OrgId: 1,
DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
})
})
Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() {
movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
So(movedDash.HasAcl, ShouldBeFalse)
Convey("should return folder without acl and its children", func() {
query := &search.FindPersistedDashboardsQuery{
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
OrgId: 1,
DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 4)
So(query.Result[0].Id, ShouldEqual, folder2.Id)
So(query.Result[1].Id, ShouldEqual, childDash1.Id)
So(query.Result[2].Id, ShouldEqual, childDash2.Id)
So(query.Result[3].Id, ShouldEqual, dashInRoot.Id)
})
})
Convey("and a dashboard with an acl is moved to the folder without an acl", func() {
updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT)
movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
So(movedDash.HasAcl, ShouldBeTrue)
Convey("should return folder without acl but not the dashboard with acl", func() {
query := &search.FindPersistedDashboardsQuery{
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
OrgId: 1,
DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 3)
So(query.Result[0].Id, ShouldEqual, folder2.Id)
So(query.Result[1].Id, ShouldEqual, childDash2.Id)
So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
})
})
})
})
})
}
func insertTestDashboard(title string, orgId int64, folderId int64, isFolder bool, tags ...interface{}) *m.Dashboard {
cmd := m.SaveDashboardCommand{
OrgId: orgId,
FolderId: folderId,
IsFolder: isFolder,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
return cmd.Result
}
func createUser(name string, role string, isAdmin bool) m.User {
setting.AutoAssignOrg = true
setting.AutoAssignOrgRole = role
currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
err := CreateUser(&currentUserCmd)
So(err, ShouldBeNil)
q1 := m.GetUserOrgListQuery{UserId: currentUserCmd.Result.Id}
GetUserOrgList(&q1)
So(q1.Result[0].Role, ShouldEqual, role)
return currentUserCmd.Result
}
func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.PermissionType) int64 {
cmd := &m.SetDashboardAclCommand{
OrgId: 1,
UserId: userId,
DashboardId: dashId,
Permission: permissions,
}
err := SetDashboardAcl(cmd)
So(err, ShouldBeNil)
return cmd.Result.Id
}
func removeAcl(aclId int64) {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{AclId: aclId, OrgId: 1})
So(err, ShouldBeNil)
}
func moveDashboard(orgId int64, dashboard *simplejson.Json, newFolderId int64) *m.Dashboard {
cmd := m.SaveDashboardCommand{
OrgId: orgId,
FolderId: newFolderId,
Dashboard: dashboard,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
return cmd.Result
}

View File

@ -32,6 +32,10 @@ func GetDashboardVersion(query *m.GetDashboardVersionQuery) error {
// GetDashboardVersions gets all dashboard versions for the given dashboard ID.
func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error {
if query.Limit == 0 {
query.Limit = 1000
}
err := x.Table("dashboard_version").
Select(`dashboard_version.id,
dashboard_version.dashboard_id,

View File

@ -28,7 +28,7 @@ func TestGetDashboardVersion(t *testing.T) {
InitTestDB(t)
Convey("Get a Dashboard ID and version ID", func() {
savedDash := insertTestDashboard("test dash 26", 1, "diff")
savedDash := insertTestDashboard("test dash 26", 1, 0, false, "diff")
query := m.GetDashboardVersionQuery{
DashboardId: savedDash.Id,
@ -69,7 +69,7 @@ func TestGetDashboardVersion(t *testing.T) {
func TestGetDashboardVersions(t *testing.T) {
Convey("Testing dashboard versions retrieval", t, func() {
InitTestDB(t)
savedDash := insertTestDashboard("test dash 43", 1, "diff-all")
savedDash := insertTestDashboard("test dash 43", 1, 0, false, "diff-all")
Convey("Get all versions for a given Dashboard ID", func() {
query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}

View File

@ -15,6 +15,7 @@ func InitTestDB(t *testing.T) {
x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
// x.ShowSQL()
// x.ShowSQL()

View File

@ -0,0 +1,52 @@
package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addDashboardAclMigrations(mg *Migrator) {
dashboardAclV1 := Table{
Name: "dashboard_acl",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: DB_BigInt},
{Name: "dashboard_id", Type: DB_BigInt},
{Name: "user_id", Type: DB_BigInt, Nullable: true},
{Name: "user_group_id", Type: DB_BigInt, Nullable: true},
{Name: "permission", Type: DB_SmallInt, Default: "4"},
{Name: "role", Type: DB_Varchar, Length: 20, Nullable: true},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"dashboard_id"}},
{Cols: []string{"dashboard_id", "user_id"}, Type: UniqueIndex},
{Cols: []string{"dashboard_id", "user_group_id"}, Type: UniqueIndex},
},
}
mg.AddMigration("create dashboard acl table", NewAddTableMigration(dashboardAclV1))
//------- indexes ------------------
mg.AddMigration("add unique index dashboard_acl_dashboard_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[0]))
mg.AddMigration("add unique index dashboard_acl_dashboard_id_user_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[1]))
mg.AddMigration("add unique index dashboard_acl_dashboard_id_group_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[2]))
const rawSQL = `
INSERT INTO dashboard_acl
(
org_id,
dashboard_id,
permission,
role,
created,
updated
)
VALUES
(-1,-1, 1,'Viewer','2017-06-20','2017-06-20'),
(-1,-1, 2,'Editor','2017-06-20','2017-06-20')
`
mg.AddMigration("save default acl rules in dashboard_acl table", new(RawSqlMigration).
Sqlite(rawSQL).
Postgres(rawSQL).
Mysql(rawSQL))
}

View File

@ -136,4 +136,18 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Update dashboard_tag table charset", NewTableCharsetMigration("dashboard_tag", []*Column{
{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false},
}))
// add column to store folder_id for dashboard folder structure
mg.AddMigration("Add column folder_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "folder_id", Type: DB_BigInt, Nullable: true,
}))
mg.AddMigration("Add column isFolder in dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "is_folder", Type: DB_Bool, Nullable: false, Default: "0",
}))
// add column to flag if dashboard has an ACL
mg.AddMigration("Add column has_acl in dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "has_acl", Type: DB_Bool, Nullable: false, Default: "0",
}))
}

View File

@ -26,6 +26,8 @@ func AddMigrations(mg *Migrator) {
addAnnotationMig(mg)
addTestDataMigrations(mg)
addDashboardVersionMigration(mg)
addUserGroupMigrations(mg)
addDashboardAclMigrations(mg)
}
func addMigrationLogMigrations(mg *Migrator) {

View File

@ -0,0 +1,48 @@
package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addUserGroupMigrations(mg *Migrator) {
userGroupV1 := Table{
Name: "user_group",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "org_id", Type: DB_BigInt},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"org_id"}},
{Cols: []string{"org_id", "name"}, Type: UniqueIndex},
},
}
mg.AddMigration("create user group table", NewAddTableMigration(userGroupV1))
//------- indexes ------------------
mg.AddMigration("add index user_group.org_id", NewAddIndexMigration(userGroupV1, userGroupV1.Indices[0]))
mg.AddMigration("add unique index user_group_org_id_name", NewAddIndexMigration(userGroupV1, userGroupV1.Indices[1]))
userGroupMemberV1 := Table{
Name: "user_group_member",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: DB_BigInt},
{Name: "user_group_id", Type: DB_BigInt},
{Name: "user_id", Type: DB_BigInt},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"org_id"}},
{Cols: []string{"org_id", "user_group_id", "user_id"}, Type: UniqueIndex},
},
}
mg.AddMigration("create user group member table", NewAddTableMigration(userGroupMemberV1))
//------- indexes ------------------
mg.AddMigration("add index user_group_member.org_id", NewAddIndexMigration(userGroupMemberV1, userGroupMemberV1.Indices[0]))
mg.AddMigration("add unique index user_group_member_org_id_user_group_id_user_id", NewAddIndexMigration(userGroupMemberV1, userGroupMemberV1.Indices[1]))
}

View File

@ -154,6 +154,57 @@ func TestAccountDataAccess(t *testing.T) {
So(err, ShouldEqual, m.ErrLastOrgAdmin)
})
Convey("Given an org user with dashboard permissions", func() {
ac3cmd := m.CreateUserCommand{Login: "ac3", Email: "ac3@test.com", Name: "ac3 name", IsAdmin: false}
err := CreateUser(&ac3cmd)
So(err, ShouldBeNil)
ac3 := ac3cmd.Result
orgUserCmd := m.AddOrgUserCommand{
OrgId: ac1.OrgId,
UserId: ac3.Id,
Role: m.ROLE_VIEWER,
}
err = AddOrgUser(&orgUserCmd)
So(err, ShouldBeNil)
query := m.GetOrgUsersQuery{OrgId: ac1.OrgId}
err = GetOrgUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 3)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT})
So(err, ShouldBeNil)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 2, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT})
So(err, ShouldBeNil)
Convey("When org user is deleted", func() {
cmdRemove := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac3.Id}
err := RemoveOrgUser(&cmdRemove)
So(err, ShouldBeNil)
Convey("Should remove dependent permissions for deleted org user", func() {
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: ac1.OrgId}
err = GetDashboardAclInfoList(permQuery)
So(err, ShouldBeNil)
So(len(permQuery.Result), ShouldEqual, 0)
})
Convey("Should not remove dashboard permissions for same user in another org", func() {
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 2, OrgId: ac3.OrgId}
err = GetDashboardAclInfoList(permQuery)
So(err, ShouldBeNil)
So(len(permQuery.Result), ShouldEqual, 1)
So(permQuery.Result[0].OrgId, ShouldEqual, ac3.OrgId)
So(permQuery.Result[0].UserId, ShouldEqual, ac3.Id)
})
})
})
})
})
})

View File

@ -80,10 +80,17 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error {
func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
return inTransaction(func(sess *DBSession) error {
var rawSql = "DELETE FROM org_user WHERE org_id=? and user_id=?"
_, err := sess.Exec(rawSql, cmd.OrgId, cmd.UserId)
if err != nil {
return err
deletes := []string{
"DELETE FROM org_user WHERE org_id=? and user_id=?",
"DELETE FROM dashboard_acl WHERE org_id=? and user_id = ?",
"DELETE FROM user_group_member WHERE org_id=? and user_id = ?",
}
for _, sql := range deletes {
_, err := sess.Exec(sql, cmd.OrgId, cmd.UserId)
if err != nil {
return err
}
}
return validateOneAdminLeftInOrg(cmd.OrgId, sess)

View File

@ -396,6 +396,10 @@ func DeleteUser(cmd *m.DeleteUserCommand) error {
deletes := []string{
"DELETE FROM star WHERE user_id = ?",
"DELETE FROM " + dialect.Quote("user") + " WHERE id = ?",
"DELETE FROM org_user WHERE user_id = ?",
"DELETE FROM dashboard_acl WHERE user_id = ?",
"DELETE FROM preferences WHERE user_id = ?",
"DELETE FROM user_group_member WHERE user_id = ?",
}
for _, sql := range deletes {

View File

@ -0,0 +1,233 @@
package sqlstore
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
func init() {
bus.AddHandler("sql", CreateUserGroup)
bus.AddHandler("sql", UpdateUserGroup)
bus.AddHandler("sql", DeleteUserGroup)
bus.AddHandler("sql", SearchUserGroups)
bus.AddHandler("sql", GetUserGroupById)
bus.AddHandler("sql", GetUserGroupsByUser)
bus.AddHandler("sql", AddUserGroupMember)
bus.AddHandler("sql", RemoveUserGroupMember)
bus.AddHandler("sql", GetUserGroupMembers)
}
func CreateUserGroup(cmd *m.CreateUserGroupCommand) error {
return inTransaction(func(sess *DBSession) error {
if isNameTaken, err := isUserGroupNameTaken(cmd.Name, 0, sess); err != nil {
return err
} else if isNameTaken {
return m.ErrUserGroupNameTaken
}
userGroup := m.UserGroup{
Name: cmd.Name,
OrgId: cmd.OrgId,
Created: time.Now(),
Updated: time.Now(),
}
_, err := sess.Insert(&userGroup)
cmd.Result = userGroup
return err
})
}
func UpdateUserGroup(cmd *m.UpdateUserGroupCommand) error {
return inTransaction(func(sess *DBSession) error {
if isNameTaken, err := isUserGroupNameTaken(cmd.Name, cmd.Id, sess); err != nil {
return err
} else if isNameTaken {
return m.ErrUserGroupNameTaken
}
userGroup := m.UserGroup{
Name: cmd.Name,
Updated: time.Now(),
}
affectedRows, err := sess.Id(cmd.Id).Update(&userGroup)
if err != nil {
return err
}
if affectedRows == 0 {
return m.ErrUserGroupNotFound
}
return nil
})
}
func DeleteUserGroup(cmd *m.DeleteUserGroupCommand) error {
return inTransaction(func(sess *DBSession) error {
if res, err := sess.Query("SELECT 1 from user_group WHERE id=?", cmd.Id); err != nil {
return err
} else if len(res) != 1 {
return m.ErrUserGroupNotFound
}
deletes := []string{
"DELETE FROM user_group_member WHERE user_group_id = ?",
"DELETE FROM user_group WHERE id = ?",
"DELETE FROM dashboard_acl WHERE user_group_id = ?",
}
for _, sql := range deletes {
_, err := sess.Exec(sql, cmd.Id)
if err != nil {
return err
}
}
return nil
})
}
func isUserGroupNameTaken(name string, existingId int64, sess *DBSession) (bool, error) {
var userGroup m.UserGroup
exists, err := sess.Where("name=?", name).Get(&userGroup)
if err != nil {
return false, nil
}
if exists && existingId != userGroup.Id {
return true, nil
}
return false, nil
}
func SearchUserGroups(query *m.SearchUserGroupsQuery) error {
query.Result = m.SearchUserGroupQueryResult{
UserGroups: make([]*m.UserGroup, 0),
}
queryWithWildcards := "%" + query.Query + "%"
sess := x.Table("user_group")
sess.Where("org_id=?", query.OrgId)
if query.Query != "" {
sess.Where("name LIKE ?", queryWithWildcards)
}
if query.Name != "" {
sess.Where("name=?", query.Name)
}
sess.Asc("name")
offset := query.Limit * (query.Page - 1)
sess.Limit(query.Limit, offset)
sess.Cols("id", "name")
if err := sess.Find(&query.Result.UserGroups); err != nil {
return err
}
userGroup := m.UserGroup{}
countSess := x.Table("user_group")
if query.Query != "" {
countSess.Where("name LIKE ?", queryWithWildcards)
}
if query.Name != "" {
countSess.Where("name=?", query.Name)
}
count, err := countSess.Count(&userGroup)
query.Result.TotalCount = count
return err
}
func GetUserGroupById(query *m.GetUserGroupByIdQuery) error {
var userGroup m.UserGroup
exists, err := x.Id(query.Id).Get(&userGroup)
if err != nil {
return err
}
if !exists {
return m.ErrUserGroupNotFound
}
query.Result = &userGroup
return nil
}
func GetUserGroupsByUser(query *m.GetUserGroupsByUserQuery) error {
query.Result = make([]*m.UserGroup, 0)
sess := x.Table("user_group")
sess.Join("INNER", "user_group_member", "user_group.id=user_group_member.user_group_id")
sess.Where("user_group_member.user_id=?", query.UserId)
err := sess.Find(&query.Result)
if err != nil {
return err
}
return nil
}
func AddUserGroupMember(cmd *m.AddUserGroupMemberCommand) error {
return inTransaction(func(sess *DBSession) error {
if res, err := sess.Query("SELECT 1 from user_group_member WHERE user_group_id=? and user_id=?", cmd.UserGroupId, cmd.UserId); err != nil {
return err
} else if len(res) == 1 {
return m.ErrUserGroupMemberAlreadyAdded
}
if res, err := sess.Query("SELECT 1 from user_group WHERE id=?", cmd.UserGroupId); err != nil {
return err
} else if len(res) != 1 {
return m.ErrUserGroupNotFound
}
entity := m.UserGroupMember{
OrgId: cmd.OrgId,
UserGroupId: cmd.UserGroupId,
UserId: cmd.UserId,
Created: time.Now(),
Updated: time.Now(),
}
_, err := sess.Insert(&entity)
return err
})
}
func RemoveUserGroupMember(cmd *m.RemoveUserGroupMemberCommand) error {
return inTransaction(func(sess *DBSession) error {
var rawSql = "DELETE FROM user_group_member WHERE user_group_id=? and user_id=?"
_, err := sess.Exec(rawSql, cmd.UserGroupId, cmd.UserId)
if err != nil {
return err
}
return err
})
}
func GetUserGroupMembers(query *m.GetUserGroupMembersQuery) error {
query.Result = make([]*m.UserGroupMemberDTO, 0)
sess := x.Table("user_group_member")
sess.Join("INNER", "user", fmt.Sprintf("user_group_member.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("user_group_member.user_group_id=?", query.UserGroupId)
sess.Cols("user.org_id", "user_group_member.user_group_id", "user_group_member.user_id", "user.email", "user.login")
sess.Asc("user.login", "user.email")
err := sess.Find(&query.Result)
return err
}

View File

@ -0,0 +1,114 @@
package sqlstore
import (
"fmt"
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestUserGroupCommandsAndQueries(t *testing.T) {
Convey("Testing User Group commands & queries", t, func() {
InitTestDB(t)
Convey("Given saved users and two user groups", func() {
var userIds []int64
for i := 0; i < 5; i++ {
userCmd := &m.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
}
err := CreateUser(userCmd)
So(err, ShouldBeNil)
userIds = append(userIds, userCmd.Result.Id)
}
group1 := m.CreateUserGroupCommand{Name: "group1 name"}
group2 := m.CreateUserGroupCommand{Name: "group2 name"}
err := CreateUserGroup(&group1)
So(err, ShouldBeNil)
err = CreateUserGroup(&group2)
So(err, ShouldBeNil)
Convey("Should be able to create user groups and add users", func() {
query := &m.SearchUserGroupsQuery{Name: "group1 name", Page: 1, Limit: 10}
err = SearchUserGroups(query)
So(err, ShouldBeNil)
So(query.Page, ShouldEqual, 1)
userGroup1 := query.Result.UserGroups[0]
So(userGroup1.Name, ShouldEqual, "group1 name")
err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: userGroup1.Id, UserId: userIds[0]})
So(err, ShouldBeNil)
q1 := &m.GetUserGroupMembersQuery{UserGroupId: userGroup1.Id}
err = GetUserGroupMembers(q1)
So(err, ShouldBeNil)
So(q1.Result[0].UserGroupId, ShouldEqual, userGroup1.Id)
So(q1.Result[0].Login, ShouldEqual, "loginuser0")
})
Convey("Should be able to search for user groups", func() {
query := &m.SearchUserGroupsQuery{Query: "group", Page: 1}
err = SearchUserGroups(query)
So(err, ShouldBeNil)
So(len(query.Result.UserGroups), ShouldEqual, 2)
So(query.Result.TotalCount, ShouldEqual, 2)
query2 := &m.SearchUserGroupsQuery{Query: ""}
err = SearchUserGroups(query2)
So(err, ShouldBeNil)
So(len(query2.Result.UserGroups), ShouldEqual, 2)
})
Convey("Should be able to return all user groups a user is member of", func() {
groupId := group2.Result.Id
err := AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: groupId, UserId: userIds[0]})
query := &m.GetUserGroupsByUserQuery{UserId: userIds[0]}
err = GetUserGroupsByUser(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Name, ShouldEqual, "group2 name")
})
Convey("Should be able to remove users from a group", func() {
err = RemoveUserGroupMember(&m.RemoveUserGroupMemberCommand{UserGroupId: group1.Result.Id, UserId: userIds[0]})
So(err, ShouldBeNil)
q1 := &m.GetUserGroupMembersQuery{UserGroupId: group1.Result.Id}
err = GetUserGroupMembers(q1)
So(err, ShouldBeNil)
So(len(q1.Result), ShouldEqual, 0)
})
Convey("Should be able to remove a group with users and permissions", func() {
groupId := group2.Result.Id
err := AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: groupId, UserId: userIds[1]})
So(err, ShouldBeNil)
err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: groupId, UserId: userIds[2]})
So(err, ShouldBeNil)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permission: m.PERMISSION_EDIT, UserGroupId: groupId})
err = DeleteUserGroup(&m.DeleteUserGroupCommand{Id: groupId})
So(err, ShouldBeNil)
query := &m.GetUserGroupByIdQuery{Id: groupId}
err = GetUserGroupById(query)
So(err, ShouldEqual, m.ErrUserGroupNotFound)
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1}
err = GetDashboardAclInfoList(permQuery)
So(err, ShouldBeNil)
So(len(permQuery.Result), ShouldEqual, 0)
})
})
})
}

View File

@ -6,7 +6,7 @@ import (
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
)
func TestUserDataAccess(t *testing.T) {
@ -14,80 +14,134 @@ func TestUserDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
var err error
for i := 0; i < 5; i++ {
err = CreateUser(&models.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
Convey("Given 5 users", func() {
var err error
var cmd *m.CreateUserCommand
users := []m.User{}
for i := 0; i < 5; i++ {
cmd = &m.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
}
err = CreateUser(cmd)
So(err, ShouldBeNil)
users = append(users, cmd.Result)
}
Convey("Can return the first page of users and a total count", func() {
query := m.SearchUsersQuery{Query: "", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
})
So(err, ShouldBeNil)
}
Convey("Can return the first page of users and a total count", func() {
query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3}
err = SearchUsers(&query)
Convey("Can return the second page of users and a total count", func() {
query := m.SearchUsersQuery{Query: "", Page: 2, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
})
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 2)
So(query.Result.TotalCount, ShouldEqual, 5)
})
Convey("Can return the second page of users and a total count", func() {
query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3}
err = SearchUsers(&query)
Convey("Can return list of users matching query on user name", func() {
query := m.SearchUsersQuery{Query: "use", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 2)
So(query.Result.TotalCount, ShouldEqual, 5)
})
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
Convey("Can return list of users matching query on user name", func() {
query := models.SearchUsersQuery{Query: "use", Page: 1, Limit: 3}
err = SearchUsers(&query)
query = m.SearchUsersQuery{Query: "ser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
query = models.SearchUsersQuery{Query: "ser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
query = m.SearchUsersQuery{Query: "USER1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
query = models.SearchUsersQuery{Query: "USER1", Page: 1, Limit: 3}
err = SearchUsers(&query)
query = m.SearchUsersQuery{Query: "idontexist", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 0)
So(query.Result.TotalCount, ShouldEqual, 0)
})
query = models.SearchUsersQuery{Query: "idontexist", Page: 1, Limit: 3}
err = SearchUsers(&query)
Convey("Can return list of users matching query on email", func() {
query := m.SearchUsersQuery{Query: "ser1@test.com", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 0)
So(query.Result.TotalCount, ShouldEqual, 0)
})
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
})
Convey("Can return list of users matching query on email", func() {
query := models.SearchUsersQuery{Query: "ser1@test.com", Page: 1, Limit: 3}
err = SearchUsers(&query)
Convey("Can return list of users matching query on login name", func() {
query := m.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
})
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
})
Convey("Can return list of users matching query on login name", func() {
query := models.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
Convey("when a user is an org member and has been assigned permissions", func() {
err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId})
So(err, ShouldBeNil)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT})
So(err, ShouldBeNil)
err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"})
So(err, ShouldBeNil)
Convey("when the user is deleted", func() {
err = DeleteUser(&m.DeleteUserCommand{UserId: users[0].Id})
So(err, ShouldBeNil)
Convey("Should delete connected org users and permissions", func() {
query := &m.GetOrgUsersQuery{OrgId: 1}
err = GetOrgUsersForTest(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1}
err = GetDashboardAclInfoList(permQuery)
So(err, ShouldBeNil)
So(len(permQuery.Result), ShouldEqual, 0)
prefsQuery := &m.GetPreferencesQuery{OrgId: users[0].OrgId, UserId: users[0].Id}
err = GetPreferences(prefsQuery)
So(err, ShouldBeNil)
So(prefsQuery.Result.OrgId, ShouldEqual, 0)
So(prefsQuery.Result.UserId, ShouldEqual, 0)
})
})
})
})
})
}
func GetOrgUsersForTest(query *m.GetOrgUsersQuery) error {
query.Result = make([]*m.OrgUserDTO, 0)
sess := x.Table("org_user")
sess.Join("LEFT ", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("org_user.org_id=?", query.OrgId)
sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role")
err := sess.Find(&query.Result)
return err
}

View File

@ -199,14 +199,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
}
}
// hide menus
var openMenus = body.find('.navbar-page-btn--open');
if (openMenus.length > 0) {
if (target.parents('.navbar-page-btn--open').length === 0) {
openMenus.removeClass('navbar-page-btn--open');
}
}
// hide sidemenu
if (!ignoreSideMenuHide && !contextSrv.pinned && body.find('.sidemenu').length > 0) {
if (target.parents('.sidemenu').length === 0) {

View File

@ -3,15 +3,8 @@
<span class="navbar-brand-btn-background">
<img src="public/img/grafana_icon.svg"></img>
</span>
<i class="icon-gf icon-gf-grafana_wordmark"></i>
<i class="fa fa-caret-down"></i>
<i class="fa fa-chevron-left"></i>
</a>
<!-- <a class="navbar&#45;page&#45;btn navbar&#45;page&#45;btn&#45;&#45;search" ng&#45;click="ctrl.showSearch()"> -->
<!-- <i class="fa fa&#45;search"></i> -->
<!-- </a> -->
<div ng-if="::!ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn">
<i class="{{::ctrl.section.icon}}" ng-show="::ctrl.section.icon"></i>
@ -20,7 +13,7 @@
</a>
</div>
<div class="dropdown navbar-section-wrapper" ng-if="::ctrl.hasMenu">
<div class="dropdown navbar-page-btn-wrapper" ng-if="::ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn" data-toggle="dropdown">
<i class="{{::ctrl.section.icon}}" ng-show="::ctrl.section.icon"></i>
<img ng-src="{{::ctrl.section.iconUrl}}" ng-show="::ctrl.section.iconUrl"></i>
@ -28,7 +21,7 @@
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu--navbar">
<li ng-repeat="navItem in ::ctrl.model.menu" ng-class="{active: navItem.active}">
<li ng-repeat="navItem in ::ctrl.model.menu">
<a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
<i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
{{::navItem.title}}

View File

@ -4,9 +4,6 @@
<div class="search-container" ng-if="ctrl.isOpen">
<div class="search-field-wrapper">
<div class="search-field-icon pointer" ng-click="ctrl.closeSearch()">
<i class="fa fa-search"></i>
</div>
<input type="text" placeholder="Find dashboards by name" give-focus="ctrl.giveSearchFocus" tabindex="1"
ng-keydown="ctrl.keyDown($event)"
@ -56,36 +53,21 @@
<div class="search-results-container" ng-if="!ctrl.tagsMode">
<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
<a class="search-item pointer search-item-{{row.type}}" bindonce ng-repeat="row in ctrl.results"
ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
<div ng-repeat="row in ctrl.results">
<a class="search-item search-item--{{::row.type}}" ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
<span class="search-result-tags">
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
</span>
<span class="search-result-tags">
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
</span>
<span class="search-result-link">
<i class="fa search-result-icon"></i>
<span bo-text="row.title"></span>
</span>
</a>
</div>
<div class="search-button-row">
<a class="btn btn-secondary" href="dashboard/new" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
<i class="fa fa-plus"></i>&nbsp; New Dashboard
</a>
<a class="btn btn-inverse" href="dashboard/new/?editview=import" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
<i class="fa fa-upload"></i>&nbsp; Import Dashboard
</a>
<a class="search-button-row-explore-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
Find <img src="public/img/icn-dashboard-tiny.svg" width="14" /> dashboards on Grafana.com
</a>
</div>
<span class="search-result-link">
<i class="fa search-result-icon"></i>
{{::row.title}}
</span>
</a>
</div>
</div>
</div>

View File

@ -30,19 +30,21 @@ export class SearchCtrl {
closeSearch() {
this.isOpen = this.ignoreClose;
this.openCompleted = false;
this.contextSrv.isSearching = this.isOpen;
}
openSearch(evt, payload) {
if (this.isOpen) {
this.isOpen = false;
this.closeSearch();
return;
}
this.isOpen = true;
this.contextSrv.isSearching = true;
this.giveSearchFocus = 0;
this.selectedIndex = -1;
this.results = [];
this.query = { query: '', tag: [], starred: false };
this.query = { query: '', tag: [], starred: false, mode: 'tree' };
this.currentSearchId = 0;
this.ignoreClose = true;
@ -104,17 +106,49 @@ export class SearchCtrl {
this.currentSearchId = this.currentSearchId + 1;
var localSearchId = this.currentSearchId;
return this.backendSrv.search(this.query).then((results) => {
return this.backendSrv.search(this.query).then(results => {
if (localSearchId < this.currentSearchId) { return; }
this.results = _.map(results, function(dash) {
dash.url = 'dashboard/' + dash.uri;
return dash;
let byId = _.groupBy(results, 'id');
let byFolderId = _.groupBy(results, 'folderId');
let finalList = [];
// add missing parent folders
_.each(results, (hit, index) => {
if (hit.folderId && !byId[hit.folderId]) {
const folder = {
id: hit.folderId,
uri: `db/${hit.folderSlug}`,
title: hit.folderTitle,
type: 'dash-folder'
};
byId[hit.folderId] = folder;
results.splice(index, 0, folder);
}
});
if (this.queryHasNoFilters()) {
this.results.unshift({ title: 'Home', url: config.appSubUrl + '/', type: 'dash-home' });
// group by folder
for (let hit of results) {
if (hit.folderId) {
hit.type = "dash-child";
} else {
finalList.push(hit);
}
hit.url = 'dashboard/' + hit.uri;
if (hit.type === 'dash-folder') {
if (!byFolderId[hit.id]) {
continue;
}
for (let child of byFolderId[hit.id]) {
finalList.push(child);
}
}
}
this.results = finalList;
});
}

View File

@ -1,50 +1,22 @@
<ul class="sidemenu">
<li class="sidemenu-org-section" ng-if="::ctrl.isSignedIn" class="dropdown">
<a class="sidemenu-org" href="profile">
<div class="sidemenu-org-avatar">
<img ng-src="{{::ctrl.user.gravatarUrl}}">
<span class="sidemenu-org-avatar--missing">
<i class="fa fa-fw fa-user"></i>
</span>
</div>
<div class="sidemenu-org-details">
<span class="sidemenu-org-user sidemenu-item-text">{{::ctrl.user.name}}</span>
<span class="sidemenu-org-name sidemenu-item-text">{{::ctrl.user.orgName}}</span>
</div>
<li>
<a class="sidemenu-item" ng-click="ctrl.search()">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-search"></i></span>
</a>
<i class="fa fa-caret-right"></i>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="menuItem in ctrl.orgMenu" ng-class="::menuItem.cssClass">
<span ng-show="::menuItem.section">{{::menuItem.section}}</span>
<a href="{{::menuItem.url}}" ng-show="::menuItem.url" target="{{::menuItem.target}}">
<i class="{{::menuItem.icon}}" ng-show="::menuItem.icon"></i>
{{::menuItem.text}}
</a>
</li>
<li ng-show="ctrl.orgs.length > ctrl.maxShownOrgs" style="margin-left: 10px;width: 90%">
<span class="sidemenu-item-text">Max shown : {{::ctrl.maxShownOrgs}}</span>
<input ng-model="::ctrl.orgFilter" style="padding-left: 5px" type="text" ng-change="::ctrl.loadOrgsItems();" class="gf-input-small width-12" placeholder="Filter">
</li>
<li ng-repeat="orgItem in ctrl.orgItems" ng-class="::orgItem.cssClass">
<a href="{{::orgItem.url}}" ng-show="::orgItem.url" target="{{::orgItem.target}}">
<i class="{{::orgItem.icon}}" ng-show="::orgItem.icon"></i>
{{::orgItem.text}}
</a>
</li>
</ul>
</li>
<li ng-repeat="item in ::ctrl.mainLinks" class="dropdown">
<a href="{{::item.url}}" class="sidemenu-item sidemenu-main-link" target="{{::item.target}}">
<a href="{{::item.url}}" class="sidemenu-item" target="{{::item.target}}">
<span class="icon-circle sidemenu-icon">
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
<img ng-src="{{::item.img}}" ng-show="::item.img">
</span>
<span class="sidemenu-item-text">{{::item.text}}</span>
<span class="fa fa-caret-right" ng-if="::item.children"></span>
</a>
<ul class="dropdown-menu" role="menu" ng-if="::item.children">
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
<li class="side-menu-header">
<span class="sidemenu-item-text">{{::item.text}}</span>
</li>
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
<a href="{{::child.url}}">
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
@ -55,17 +27,45 @@
</li>
<li ng-show="::!ctrl.isSignedIn">
<a href="{{ctrl.loginUrl}}" class="sidemenu-item" target="_self">
<a href="{{ctrl.loginUrl}}" class="sidemenu-item" target="_self">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-sign-in"></i></span>
<span class="sidemenu-item-text">Sign in</span>
</a>
</li>
<li>
<a class="sidemenu-item" target="_self" ng-hide="ctrl.contextSrv.pinned" ng-click="ctrl.contextSrv.setPinnedState(true)">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-thumb-tack"></i></span>
<span class="sidemenu-item-text">Pin</span>
<li class="sidemenu-org-section" ng-if="::ctrl.isSignedIn" class="dropdown">
<a class="sidemenu-item" href="profile">
<span class="icon-circle sidemenu-icon sidemenu-org-avatar">
<img ng-src="{{::ctrl.user.gravatarUrl}}">
<span class="sidemenu-org-avatar--missing">
<i class="fa fa-fw fa-user"></i>
</span>
</div>
</a>
<ul class="dropdown-menu dropdown-menu--sidemenu dropup" role="menu">
<li class="side-menu-header">
<span class="sidemenu-org-user sidemenu-item-text">{{::ctrl.user.name}}</span>
<span class="sidemenu-org-name sidemenu-item-text">{{::ctrl.user.orgName}}</span>
</li>
<li ng-repeat="menuItem in ctrl.orgMenu" ng-class="::menuItem.cssClass">
<span ng-show="::menuItem.section">{{::menuItem.section}}</span>
<a href="{{::menuItem.url}}" ng-show="::menuItem.url" target="{{::menuItem.target}}">
<i class="{{::menuItem.icon}}" ng-show="::menuItem.icon"></i>
{{::menuItem.text}}
</a>
</li>
<li ng-show="ctrl.orgs.length > ctrl.maxShownOrgs" style="margin-left: 10px;width: 90%">
<span class="sidemenu-item-text">Max shown : {{::ctrl.maxShownOrgs}}</span>
<input ng-model="::ctrl.orgFilter" style="padding-left: 5px" type="text" ng-change="::ctrl.loadOrgsItems();" class="gf-input-small width-12" placeholder="Filter">
</li>
<li ng-repeat="orgItem in ctrl.orgItems" ng-class="::orgItem.cssClass">
<a href="{{::orgItem.url}}" ng-show="::orgItem.url" target="{{::orgItem.target}}">
<i class="{{::orgItem.icon}}" ng-show="::orgItem.icon"></i>
{{::orgItem.text}}
</a>
</li>
</ul>
</li>
</ul>

View File

@ -19,7 +19,7 @@ export class SideMenuCtrl {
maxShownOrgs: number;
/** @ngInject */
constructor(private $scope, private $location, private contextSrv, private backendSrv, private $element) {
constructor(private $scope, private $rootScope, private $location, private contextSrv, private backendSrv, private $element) {
this.isSignedIn = contextSrv.isSignedIn;
this.user = contextSrv.user;
this.appSubUrl = config.appSubUrl;
@ -44,6 +44,10 @@ export class SideMenuCtrl {
return config.appSubUrl + url;
}
search() {
this.$rootScope.appEvent('show-dash-search');
}
openUserDropdown() {
this.orgMenu = [
{section: 'You', cssClass: 'dropdown-menu-title'},
@ -64,6 +68,10 @@ export class SideMenuCtrl {
text: "Users",
url: this.getUrl("/org/users")
});
this.orgMenu.push({
text: "User Groups",
url: this.getUrl("/org/user-groups")
});
this.orgMenu.push({
text: "API Keys",
url: this.getUrl("/org/apikeys")

View File

@ -0,0 +1,60 @@
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
const template = `
<div class="dropdown">
<gf-form-dropdown model="ctrl.group"
get-options="ctrl.debouncedSearchGroups($query)"
css-class="gf-size-auto"
on-change="ctrl.onChange($option)"
</gf-form-dropdown>
</div>
`;
export class UserGroupPickerCtrl {
group: any;
userGroupPicked: any;
debouncedSearchGroups: any;
/** @ngInject */
constructor(private backendSrv, private $scope, $sce, private uiSegmentSrv) {
this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {'leading': true, 'trailing': false});
this.reset();
}
reset() {
this.group = {text: 'Choose', value: null};
}
searchGroups(query: string) {
return Promise.resolve(this.backendSrv.get('/api/user-groups/search?perpage=10&page=1&query=' + query).then(result => {
return _.map(result.userGroups, ug => {
return {text: ug.name, value: ug};
});
}));
}
onChange(option) {
this.userGroupPicked({$group: option.value});
}
}
export function userGroupPicker() {
return {
restrict: 'E',
template: template,
controller: UserGroupPickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
userGroupPicked: '&',
},
link: function(scope, elem, attrs, ctrl) {
scope.$on("user-group-picker-reset", () => {
ctrl.reset();
});
}
};
}
coreModule.directive('userGroupPicker', userGroupPicker);

View File

@ -0,0 +1,67 @@
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
const template = `
<div class="dropdown">
<gf-form-dropdown model="ctrl.user"
get-options="ctrl.debouncedSearchUsers($query)"
css-class="gf-size-auto"
on-change="ctrl.onChange($option)"
</gf-form-dropdown>
</div>
`;
export class UserPickerCtrl {
user: any;
debouncedSearchUsers: any;
userPicked: any;
/** @ngInject */
constructor(private backendSrv, private $scope, $sce) {
this.reset();
this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {'leading': true, 'trailing': false});
}
searchUsers(query: string) {
return Promise.resolve(this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => {
return _.map(result.users, user => {
return {text: user.login + ' - ' + user.email, value: user};
});
}));
}
onChange(option) {
this.userPicked({$user: option.value});
}
reset() {
this.user = {text: 'Choose', value: null};
}
}
export interface User {
id: number;
name: string;
login: string;
email: string;
}
export function userPicker() {
return {
restrict: 'E',
template: template,
controller: UserPickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
userPicked: '&',
},
link: function(scope, elem, attrs, ctrl) {
scope.$on("user-picker-reset", () => {
ctrl.reset();
});
}
};
}
coreModule.directive('userPicker', userPicker);

View File

@ -49,7 +49,8 @@ import {helpModal} from './components/help/help';
import {collapseBox} from './components/collapse_box';
import {JsonExplorer} from './components/json_explorer/json_explorer';
import {NavModelSrv, NavModel} from './nav_model_srv';
import {userPicker} from './components/user_picker';
import {userGroupPicker} from './components/user_group_picker';
export {
arrayJoin,
@ -78,4 +79,6 @@ export {
JsonExplorer,
NavModelSrv,
NavModel,
userPicker,
userGroupPicker,
};

View File

@ -2,8 +2,9 @@ define([
'jquery',
'angular',
'../core_module',
'lodash',
],
function ($, angular, coreModule) {
function ($, angular, coreModule, _) {
'use strict';
var editViewMap = {
@ -12,7 +13,13 @@ function ($, angular, coreModule) {
'templating': { src: 'public/app/features/templating/partials/editor.html'},
'history': { html: '<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>'},
'timepicker': { src: 'public/app/features/dashboard/timepicker/dropdown.html' },
'import': { html: '<dash-import></dash-import>' }
'import': { html: '<dash-import dismiss="dismiss()"></dash-import>', isModal: true },
'permissions': { html: '<dash-acl-modal dismiss="dismiss()"></dash-acl-modal>', isModal: true },
'new-folder': {
isModal: true,
html: '<folder-modal dismiss="dismiss()"></folder-modal>',
modalClass: 'modal--narrow'
}
};
coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) {
@ -20,6 +27,7 @@ function ($, angular, coreModule) {
restrict: 'A',
link: function(scope, elem) {
var editorScope;
var modalScope;
var lastEditView;
function hideEditorPane(hideToShowOtherView) {
@ -30,8 +38,7 @@ function ($, angular, coreModule) {
function showEditorPane(evt, options) {
if (options.editview) {
options.src = editViewMap[options.editview].src;
options.html = editViewMap[options.editview].html;
_.defaults(options, editViewMap[options.editview]);
}
if (lastEditView && lastEditView === options.editview) {
@ -45,6 +52,11 @@ function ($, angular, coreModule) {
editorScope = options.scope ? options.scope.$new() : scope.$new();
editorScope.dismiss = function(hideToShowOtherView) {
if (modalScope) {
modalScope.dismiss();
modalScope = null;
}
editorScope.$destroy();
lastEditView = null;
editorScope = null;
@ -73,16 +85,17 @@ function ($, angular, coreModule) {
}
};
if (options.editview === 'import') {
var modalScope = $rootScope.$new();
if (options.isModal) {
modalScope = $rootScope.$new();
modalScope.$on("$destroy", function() {
editorScope.dismiss();
});
$rootScope.appEvent('show-modal', {
templateHtml: '<dash-import></dash-import>',
templateHtml: options.html,
scope: modalScope,
backdrop: 'static'
backdrop: 'static',
modalClass: options.modalClass,
});
return;

View File

@ -96,6 +96,7 @@ export class NavModelSrv {
{title: 'Preferences', active: subPage === 0, url: 'org', icon: 'fa fa-fw fa-cog'},
{title: 'Org Users', active: subPage === 1, url: 'org/users', icon: 'fa fa-fw fa-users'},
{title: 'API Keys', active: subPage === 2, url: 'org/apikeys', icon: 'fa fa-fw fa-key'},
{title: 'Org User Groups', active: subPage === 3, url: 'org/user-groups', icon: 'fa fa-fw fa-users'},
]
};
}
@ -167,6 +168,14 @@ export class NavModelSrv {
clickHandler: () => dashNavCtrl.openEditView('annotations')
});
if (dashboard.meta.canAdmin) {
menu.push({
title: 'Permissions...',
icon: 'fa fa-fw fa-lock',
clickHandler: () => dashNavCtrl.openEditView('permissions')
});
}
if (!dashboard.meta.isHome) {
menu.push({
title: 'Version history',
@ -196,9 +205,9 @@ export class NavModelSrv {
clickHandler: () => dashNavCtrl.showHelpModal()
});
if (this.contextSrv.isEditor) {
if (this.contextSrv.isEditor && !dashboard.meta.isFolder) {
menu.push({
title: 'Save As ...',
title: 'Save As...',
icon: 'fa fa-fw fa-save',
clickHandler: () => dashNavCtrl.saveDashboardAs()
});

View File

@ -83,6 +83,18 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
controller : 'OrgApiKeysCtrl',
resolve: loadOrgBundle,
})
.when('/org/user-groups', {
templateUrl: 'public/app/features/org/partials/user_groups.html',
controller : 'UserGroupsCtrl',
controllerAs: 'ctrl',
resolve: loadOrgBundle,
})
.when('/org/user-groups/edit/:id', {
templateUrl: 'public/app/features/org/partials/user_group_details.html',
controller : 'UserGroupDetailsCtrl',
controllerAs: 'ctrl',
resolve: loadOrgBundle,
})
.when('/profile', {
templateUrl: 'public/app/features/org/partials/profile.html',
controller : 'ProfileCtrl',

View File

@ -211,10 +211,64 @@ export class BackendSrv {
return this.post('/api/dashboards/db/', {
dashboard: dash,
folderId: dash.folderId,
overwrite: options.overwrite === true,
message: options.message || '',
});
}
createDashboardFolder(name) {
const dash = {
title: name,
editable: true,
hideControls: true,
rows: [
{
panels: [
{
folderId: 0,
headings: false,
limit: 1000,
links: [],
query: '',
recent: false,
search: true,
span: 4,
starred: false,
tags: [],
title: 'Dashboards in this folder',
type: 'dashlist'
},
{
onlyAlertsOnDashboard: true,
span: 4,
title: 'Alerts in this folder',
type: 'alertlist'
},
{
span: 4,
title: 'Permissions for this folder',
type: 'permissionlist',
folderId: 0
}
],
showTitle: true,
title: name,
titleSize: 'h1'
}
]
};
return this.post('/api/dashboards/db/', {dashboard: dash, isFolder: true, overwrite: false})
.then(res => {
return this.getDashboard('db', res.slug);
})
.then(res => {
res.dashboard.rows[0].panels[0].folderId = res.dashboard.id;
res.dashboard.rows[0].panels[2].folderId = res.dashboard.id;
return this.saveDashboard(res.dashboard, {overwrite: false});
});
}
}
coreModule.service('backendSrv', BackendSrv);

View File

@ -64,9 +64,7 @@ export class ContextSrv {
toggleSideMenu() {
this.sidemenu = !this.sidemenu;
if (!this.sidemenu) {
this.setPinnedState(false);
}
this.setPinnedState(this.sidemenu);
}
}

View File

@ -7,11 +7,6 @@
<i class="fa fa-info-circle"></i>
How to add an alert
</a>
<a class="btn btn-inverse" href="alerting/notifications" >
<i class="fa fa-cog"></i>
Configure notifications
</a>
</div>
<div class="gf-form-group">

View File

@ -0,0 +1,126 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<i class="fa fa-lock"></i>
<span class="p-l-1">Permissions</span>
</h2>
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content">
<table class="filter-table gf-form-group">
<tr ng-repeat="acl in ctrl.items" ng-class="{'gf-form-disabled': acl.inherited}">
<td style="width: 100%;">
<i class="{{acl.icon}}"></i>
<span ng-bind-html="acl.nameHtml"></span>
</td>
<td>
<em class="muted no-wrap" ng-show="acl.inherited">Inherited from folder</em>
</td>
<td class="query-keyword">Can</td>
<td>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="acl.permission" ng-options="p.value as p.text for p in ctrl.permissionOptions" ng-change="ctrl.permissionChanged(acl)" ng-disabled="acl.inherited"></select>
</div>
</td>
<td>
<a class="btn btn-inverse btn-small" ng-click="ctrl.removeItem($index)" ng-hide="acl.inherited">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
<tr ng-show="ctrl.aclItems.length === 0">
<td colspan="4">
<em>No permissions. Will only be accessible by admins.</em>
</td>
</tr>
</table>
<div class="gf-form-inline">
<form name="addPermission" class="gf-form-group">
<h6 class="muted">Add Permission For</h6>
<div class="gf-form-inline">
<div class="gf-form">
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.newType" ng-options="p.value as p.text for p in ctrl.aclTypes" ng-change="ctrl.typeChanged()"></select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.newType === 'User'">
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
</div>
<div class="gf-form" ng-show="ctrl.newType === 'Group'">
<user-group-picker user-group-picked="ctrl.groupPicked($group)"></user-group-picker>
</div>
</div>
</form>
<div class="gf-form width-17">
<span ng-if="ctrl.error" class="text-error p-l-1">
<i class="fa fa-warning"></i>
{{ctrl.error}}
</span>
</div>
</div>
<div class="gf-form-button-row text-center">
<button type="button" class="btn btn-danger" ng-disabled="!ctrl.canUpdate" ng-click="ctrl.update()">
Update Permissions
</button>
<a class="btn-text" ng-click="ctrl.dismiss();">Close</a>
</div>
</div>
</div>
<!-- <br> -->
<!-- <br> -->
<!-- <br> -->
<!-- -->
<!-- <div class="permissionlist"> -->
<!-- <div class="permissionlist__section"> -->
<!-- <div class="permissionlist__section&#45;header"> -->
<!-- <h6>Permissions</h6> -->
<!-- </div> -->
<!-- <table class="filter&#45;table form&#45;inline"> -->
<!-- <thead> -->
<!-- <tr> -->
<!-- <th style="width: 50px;"></th> -->
<!-- <th>Name</th> -->
<!-- <th style="width: 220px;">Permission</th> -->
<!-- <th style="width: 120px"></th> -->
<!-- </tr> -->
<!-- </thead> -->
<!-- <tbody> -->
<!-- <tr ng&#45;repeat="permission in ctrl.userPermissions" class="permissionlist__item"> -->
<!-- <td><i class="fa fa&#45;fw fa&#45;user"></i></td> -->
<!-- <td>{{permission.userLogin}}</td> -->
<!-- <td class="text&#45;right"> -->
<!-- <a ng&#45;click="ctrl.removePermission(permission)" class="btn btn&#45;danger btn&#45;small"> -->
<!-- <i class="fa fa&#45;remove"></i> -->
<!-- </a> -->
<!-- </td> -->
<!-- </tr> -->
<!-- <tr ng&#45;repeat="permission in ctrl.userGroupPermissions" class="permissionlist__item"> -->
<!-- <td><i class="fa fa&#45;fw fa&#45;users"></i></td> -->
<!-- <td>{{permission.userGroup}}</td> -->
<!-- <td><select class="gf&#45;form&#45;input gf&#45;size&#45;auto" ng&#45;model="permission.permissions" ng&#45;options="p.value as p.text for p in ctrl.permissionTypeOptions" ng&#45;change="ctrl.updatePermission(permission)"></select></td> -->
<!-- <td class="text&#45;right"> -->
<!-- <a ng&#45;click="ctrl.removePermission(permission)" class="btn btn&#45;danger btn&#45;small"> -->
<!-- <i class="fa fa&#45;remove"></i> -->
<!-- </a> -->
<!-- </td> -->
<!-- </tr> -->
<!-- <tr ng&#45;repeat="role in ctrl.roles" class="permissionlist__item"> -->
<!-- <td></td> -->
<!-- <td>{{role.name}}</td> -->
<!-- <td><select class="gf&#45;form&#45;input gf&#45;size&#45;auto" ng&#45;model="role.permissions" ng&#45;options="p.value as p.text for p in ctrl.roleOptions" ng&#45;change="ctrl.updatePermission(role)"></select></td> -->
<!-- <td class="text&#45;right"> -->
<!-- -->
<!-- </td> -->
<!-- </tr> -->
<!-- </tbody> -->
<!-- </table> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->

View File

@ -0,0 +1,204 @@
///<reference path="../../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
export class AclCtrl {
dashboard: any;
items: DashboardAcl[];
permissionOptions = [
{value: 1, text: 'View'},
{value: 2, text: 'Edit'},
{value: 4, text: 'Admin'}
];
aclTypes = [
{value: 'Group', text: 'User Group'},
{value: 'User', text: 'User'},
{value: 'Viewer', text: 'Everyone With Viewer Role'},
{value: 'Editor', text: 'Everyone With Editor Role'}
];
dismiss: () => void;
newType: string;
canUpdate: boolean;
error: string;
readonly duplicateError = 'This permission exists already.';
/** @ngInject */
constructor(private backendSrv, private dashboardSrv, private $sce, private $scope) {
this.items = [];
this.resetNewType();
this.dashboard = dashboardSrv.getCurrent();
this.get(this.dashboard.id);
}
resetNewType() {
this.newType = 'Group';
}
get(dashboardId: number) {
return this.backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`)
.then(result => {
this.items = _.map(result, this.prepareViewModel.bind(this));
this.sortItems();
});
}
sortItems() {
this.items = _.orderBy(this.items, ['sortRank', 'sortName'], ['desc', 'asc']);
}
prepareViewModel(item: DashboardAcl): DashboardAcl {
item.inherited = !this.dashboard.meta.isFolder && this.dashboard.id !== item.dashboardId;
item.sortRank = 0;
if (item.userId > 0) {
item.icon = "fa fa-fw fa-user";
item.nameHtml = this.$sce.trustAsHtml(item.userLogin);
item.sortName = item.userLogin;
item.sortRank = 10;
} else if (item.userGroupId > 0) {
item.icon = "fa fa-fw fa-users";
item.nameHtml = this.$sce.trustAsHtml(item.userGroup);
item.sortName = item.userGroup;
item.sortRank = 20;
} else if (item.role) {
item.icon = "fa fa-fw fa-street-view";
item.nameHtml = this.$sce.trustAsHtml(`Everyone with <span class="query-keyword">${item.role}</span> Role`);
item.sortName = item.role;
item.sortRank = 30;
if (item.role === 'Viewer') {
item.sortRank += 1;
}
}
if (item.inherited) {
item.sortRank += 100;
}
return item;
}
update() {
var updated = [];
for (let item of this.items) {
if (item.inherited) {
continue;
}
updated.push({
id: item.id,
userId: item.userId,
userGroupId: item.userGroupId,
role: item.role,
permission: item.permission,
});
}
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, { items: updated }).then(() => {
return this.dismiss();
});
}
typeChanged() {
if (this.newType === 'Viewer' || this.newType === 'Editor') {
this.addNewItem({permission: 1, role: this.newType});
this.canUpdate = true;
this.resetNewType();
}
}
permissionChanged() {
this.canUpdate = true;
}
addNewItem(item) {
if (!this.isValid(item)) {
return;
}
this.error = '';
item.dashboardId = this.dashboard.id;
this.items.push(this.prepareViewModel(item));
this.sortItems();
this.canUpdate = true;
}
isValid(item) {
const dupe = _.find(this.items, (it) => { return this.isDuplicate(it, item); });
if (dupe) {
this.error = this.duplicateError;
return false;
}
return true;
}
isDuplicate(origItem, newItem) {
if (origItem.inherited) {
return false;
}
return (origItem.role && newItem.role && origItem.role === newItem.role) ||
(origItem.userId && newItem.userId && origItem.userId === newItem.userId) ||
(origItem.userGroupId && newItem.userGroupId && origItem.userGroupId === newItem.userGroupId);
}
userPicked(user) {
this.addNewItem({userId: user.id, userLogin: user.login, permission: 1,});
this.$scope.$broadcast('user-picker-reset');
}
groupPicked(group) {
this.addNewItem({userGroupId: group.id, userGroup: group.name, permission: 1});
this.$scope.$broadcast('user-group-picker-reset');
}
removeItem(index) {
this.items.splice(index, 1);
this.canUpdate = true;
}
}
export function dashAclModal() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/acl/acl.html',
controller: AclCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
dismiss: "&"
}
};
}
export interface FormModel {
dashboardId: number;
userId?: number;
userGroupId?: number;
PermissionType: number;
}
export interface DashboardAcl {
id?: number;
dashboardId?: number;
userId?: number;
userLogin?: string;
userEmail?: string;
userGroupId?: number;
userGroup?: string;
permission?: number;
permissionName?: string;
role?: string;
icon?: string;
nameHtml?: string;
inherited?: boolean;
sortName?: string;
sortRank?: number;
}
coreModule.directive('dashAclModal', dashAclModal);

View File

@ -0,0 +1,180 @@
import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common';
import {AclCtrl} from '../acl';
describe('AclCtrl', () => {
const ctx: any = {};
const backendSrv = {
get: sinon.stub().returns(Promise.resolve([])),
post: sinon.stub().returns(Promise.resolve([]))
};
const dashboardSrv = {
getCurrent: sinon.stub().returns({id: 1, meta: { isFolder: false }})
};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.inject(($rootScope, $controller, $q, $compile) => {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
AclCtrl.prototype.dashboard = {dashboard: {id: 1}};
ctx.ctrl = $controller(AclCtrl, {
$scope: ctx.scope,
backendSrv: backendSrv,
dashboardSrv: dashboardSrv
}, {
dismiss: () => { return; }
});
}));
describe('when permissions are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
const userItem = {
id: 2,
login: 'user2',
};
ctx.ctrl.userPicked(userItem);
const userGroupItem = {
id: 2,
name: 'ug1',
};
ctx.ctrl.groupPicked(userGroupItem);
ctx.ctrl.newType = 'Editor';
ctx.ctrl.typeChanged();
ctx.ctrl.newType = 'Viewer';
ctx.ctrl.typeChanged();
});
it('should sort the result by role, user group and user', () => {
expect(ctx.ctrl.items[0].role).to.eql('Viewer');
expect(ctx.ctrl.items[1].role).to.eql('Editor');
expect(ctx.ctrl.items[2].userGroupId).to.eql(2);
expect(ctx.ctrl.items[3].userId).to.eql(2);
});
it('should save permissions to db', (done) => {
ctx.ctrl.update().then(() => {
done();
});
expect(backendSrv.post.getCall(0).args[0]).to.eql('/api/dashboards/id/1/acl');
expect(backendSrv.post.getCall(0).args[1].items[0].role).to.eql('Viewer');
expect(backendSrv.post.getCall(0).args[1].items[0].permission).to.eql(1);
expect(backendSrv.post.getCall(0).args[1].items[1].role).to.eql('Editor');
expect(backendSrv.post.getCall(0).args[1].items[1].permission).to.eql(1);
expect(backendSrv.post.getCall(0).args[1].items[2].userGroupId).to.eql(2);
expect(backendSrv.post.getCall(0).args[1].items[2].permission).to.eql(1);
expect(backendSrv.post.getCall(0).args[1].items[3].userId).to.eql(2);
expect(backendSrv.post.getCall(0).args[1].items[3].permission).to.eql(1);
});
});
describe('when duplicate role permissions are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
ctx.ctrl.items = [];
ctx.ctrl.newType = 'Editor';
ctx.ctrl.typeChanged();
ctx.ctrl.newType = 'Editor';
ctx.ctrl.typeChanged();
});
it('should throw a validation error', () => {
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
});
it('should not add the duplicate permission', () => {
expect(ctx.ctrl.items.length).to.eql(1);
});
});
describe('when duplicate user permissions are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
ctx.ctrl.items = [];
const userItem = {
id: 2,
login: 'user2',
};
ctx.ctrl.userPicked(userItem);
ctx.ctrl.userPicked(userItem);
});
it('should throw a validation error', () => {
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
});
it('should not add the duplicate permission', () => {
expect(ctx.ctrl.items.length).to.eql(1);
});
});
describe('when duplicate user group permissions are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
ctx.ctrl.items = [];
const userGroupItem = {
id: 2,
name: 'ug1',
};
ctx.ctrl.groupPicked(userGroupItem);
ctx.ctrl.groupPicked(userGroupItem);
});
it('should throw a validation error', () => {
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
});
it('should not add the duplicate permission', () => {
expect(ctx.ctrl.items.length).to.eql(1);
});
});
describe('when one inherited and one not inherited user group permission are added', () => {
beforeEach(() => {
backendSrv.get.reset();
backendSrv.post.reset();
ctx.ctrl.items = [];
const inheritedUserGroupItem = {
id: 2,
name: 'ug1',
dashboardId: -1
};
ctx.ctrl.items.push(inheritedUserGroupItem);
const userGroupItem = {
id: 2,
name: 'ug1',
};
ctx.ctrl.groupPicked(userGroupItem);
});
it('should not throw a validation error', () => {
expect(ctx.ctrl.error).to.eql('');
});
it('should add both permissions', () => {
expect(ctx.ctrl.items.length).to.eql(2);
});
});
});

View File

@ -24,4 +24,7 @@ define([
'./ad_hoc_filters',
'./row/row_ctrl',
'./repeat_option/repeat_option',
'./acl/acl',
'./folder_picker/picker',
'./folder_modal/folder'
], function () {});

View File

@ -127,6 +127,12 @@ export class DashboardCtrl {
$scope.timezoneChanged = function() {
$rootScope.$broadcast("refresh");
};
$scope.onFolderChange = function(folder) {
$scope.dashboard.folderId = folder.id;
$scope.dashboard.meta.folderId = folder.id;
$scope.dashboard.meta.folderTitle= folder.title;
};
}
init(dashboard) {

View File

@ -113,14 +113,8 @@ export class DashboardSrv {
}
showSaveAsModal() {
var newScope = this.$rootScope.$new();
newScope.clone = this.dash.getSaveModelClone();
newScope.clone.editable = true;
newScope.clone.hideControls = false;
this.$rootScope.appEvent('show-modal', {
templateHtml: '<save-dashboard-as-modal dismiss="dismiss()"></save-dashboard-as-modal>',
scope: newScope,
modalClass: 'modal--narrow'
});
}
@ -128,7 +122,6 @@ export class DashboardSrv {
showSaveModal() {
this.$rootScope.appEvent('show-modal', {
templateHtml: '<save-dashboard-modal dismiss="dismiss()"></save-dashboard-modal>',
scope: this.$rootScope.$new(),
modalClass: 'modal--narrow'
});
}

View File

@ -1,95 +1,64 @@
<div class="navbar">
<div class="navbar-inner">
<a class="navbar-brand-btn pointer" ng-click="ctrl.toggleSideMenu()">
<span class="navbar-brand-btn-background">
<img src="public/img/grafana_icon.svg"></img>
</span>
<i class="icon-gf icon-gf-grafana_wordmark"></i>
<i class="fa fa-caret-down"></i>
<i class="fa fa-chevron-left"></i>
<navbar model="ctrl.navModel">
<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
<li>
<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
</li>
<li>
<a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
</li>
<li>
<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
</li>
</ul>
<ul class="nav pull-left dashnav-action-icons">
<li ng-show="::ctrl.dashboard.meta.canStar">
<a class="pointer" ng-click="ctrl.starDashboard()">
<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
</a>
<div class="navbar-section-wrapper">
<a class="navbar-page-btn" ng-click="ctrl.showSearch()">
<i class="icon-gf icon-gf-dashboard"></i>
{{ctrl.dashboard.title}}
<i class="fa fa-caret-down"></i>
</a>
</div>
<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
</li>
<li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown">
<a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
<ul class="dropdown-menu">
<li>
<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
</li>
<li>
<a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
</li>
<li>
<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
</li>
</ul>
<ul class="nav pull-left dashnav-action-icons">
<li ng-show="::ctrl.dashboard.meta.canStar">
<a class="pointer" ng-click="ctrl.starDashboard()">
<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
</a>
</li>
<li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown">
<a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
<ul class="dropdown-menu">
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(0)">
<i class="fa fa-link"></i> Link to Dashboard
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(1)">
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(2)">
<i class="fa fa-cloud-upload"></i>Export
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
</a>
</li>
</ul>
</li>
<li ng-show="::ctrl.dashboard.meta.canSave">
<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
</li>
<li class="dropdown">
<a class="pointer" data-toggle="dropdown">
<i class="fa fa-cog"></i>
</a>
<ul class="dropdown-menu dropdown-menu--navbar">
<li ng-repeat="navItem in ::ctrl.navModel.menu" ng-class="{active: navItem.active}">
<a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
<i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
{{::navItem.title}}
</a>
</li>
</ul>
</li>
</ul>
<ul class="nav pull-right">
<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
<a ng-click="ctrl.exitFullscreen()">
Back to dashboard
<a class="pointer" ng-click="ctrl.shareDashboard(0)">
<i class="fa fa-link"></i> Link to Dashboard
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
</a>
</li>
<li>
<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
<a class="pointer" ng-click="ctrl.shareDashboard(1)">
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(2)">
<i class="fa fa-cloud-upload"></i>Export
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
</a>
</li>
</ul>
</div>
</div>
</li>
<li ng-show="::ctrl.dashboard.meta.canSave">
<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
</li>
</ul>
<ul class="nav pull-right">
<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
<a ng-click="ctrl.exitFullscreen()">
Back to dashboard
</a>
</li>
<li>
<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
</li>
</ul>
</navbar>
<dashboard-search></dashboard-search>

View File

@ -105,7 +105,7 @@ export class DashNavCtrl {
if (alerts > 0) {
confirmText = 'DELETE';
text2 = `This dashboad contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
text2 = `This dashboard contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
}
appEvents.emit('confirm-modal', {
@ -140,15 +140,8 @@ export class DashNavCtrl {
var newWindow = window.open(uri);
}
showSearch() {
this.$rootScope.appEvent('show-dash-search');
}
navItemClicked(navItem, evt) {
if (navItem.clickHandler) {
navItem.clickHandler();
evt.preventDefault();
}
onFolderChange(folderId) {
this.dashboard.folderId = folderId;
}
}

View File

@ -0,0 +1,24 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<i class="fa fa-folder"></i>
<span class="p-l-1">New Dashboard Folder</span>
</h2>
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<form name="ctrl.saveForm" ng-submit="ctrl.create()" class="modal-content folder-modal" novalidate>
<div class="p-t-2">
<div class="gf-form">
<input type="text" ng-model="ctrl.title" required give-focus="true" class="gf-form-input" placeholder="Enter folder name" />
</div>
</div>
<div class="gf-form-button-row text-center">
<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid">Create</button>
<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
</div>
</form>
</div>

View File

@ -0,0 +1,45 @@
///<reference path="../../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
export class FolderCtrl {
title: string;
dismiss: any;
/** @ngInject */
constructor(private backendSrv, private $scope, private $location) {
}
create() {
if (!this.title || this.title.trim().length === 0) {
return;
}
const title = this.title.trim();
return this.backendSrv.createDashboardFolder(title).then(result => {
appEvents.emit('alert-success', ['Folder Created', 'OK']);
this.dismiss();
var folderUrl = '/dashboard/db/' + result.slug;
this.$location.url(folderUrl);
});
}
}
export function folderModal() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/folder_modal/folder.html',
controller: FolderCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
dismiss: "&"
}
};
}
coreModule.directive('folderModal', folderModal);

View File

@ -0,0 +1,83 @@
///<reference path="../../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
export class FolderPickerCtrl {
initialTitle: string;
initialFolderId: number;
labelClass: string;
onChange: any;
rootName = 'Root';
private folder: any;
/** @ngInject */
constructor(private backendSrv, private $scope, private $sce) {
if (!this.labelClass) {
this.labelClass = "width-7";
}
if (this.initialFolderId > 0) {
this.getOptions('').then(result => {
this.folder = _.find(result, {value: this.initialFolderId});
});
} else {
this.folder = {text: this.initialTitle, value: null};
}
}
getOptions(query) {
var params = {
query: query,
type: 'dash-folder',
};
return this.backendSrv.search(params).then(result => {
if (query === "") {
result.unshift({title: this.rootName, value: 0});
}
return _.map(result, item => {
return {text: item.title, value: item.id};
});
});
}
onFolderChange(option) {
this.onChange({$folder: {id: option.value, title: option.text}});
}
}
const template = `
<div class="gf-form">
<label class="gf-form-label {{ctrl.labelClass}}">Folder</label>
<div class="dropdown">
<gf-form-dropdown model="ctrl.folder"
get-options="ctrl.getOptions($query)"
on-change="ctrl.onFolderChange($option)">
</gf-form-dropdown>
</div>
</div>
`;
export function folderPicker() {
return {
restrict: 'E',
template: template,
controller: FolderPickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
initialTitle: "<",
initialFolderId: '<',
labelClass: '@',
rootName: '@',
onChange: '&'
}
};
}
coreModule.directive('folderPicker', folderPicker);

View File

@ -1,4 +1,3 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">

View File

@ -36,6 +36,7 @@ export class DashboardModel {
meta: any;
events: any;
editMode: boolean;
folderId: number;
constructor(data, meta?) {
if (!data) {
@ -64,6 +65,7 @@ export class DashboardModel {
this.version = data.version || 0;
this.links = data.links || [];
this.gnetId = data.gnetId || null;
this.folderId = data.folderId || null;
this.rows = [];
if (data.rows) {

View File

@ -4,7 +4,7 @@
</h2>
<ul class="gf-tabs">
<li class="gf-tabs-item" ng-repeat="tab in ::['General', 'Rows', 'Links', 'Time picker', 'Metadata']">
<li class="gf-tabs-item" ng-repeat="tab in ::['General', 'Links', 'Time picker']">
<a class="gf-tabs-link" ng-click="editor.index = $index" ng-class="{active: editor.index === $index}">
{{::tab}}
</a>
@ -38,17 +38,22 @@
</bootstrap-tagsinput>
</div>
<div class="gf-form">
<label class="gf-form-label width-7">Timezone</label>
<div class="gf-form-select-wrapper">
<select ng-model="dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
</div>
</div>
<folder-picker ng-if="!dashboardMeta.isFolder"
initial-title="dashboardMeta.folderTitle"
on-change="onFolderChange($folder)"
label-class="width-7">
</folder-picker>
</div>
<div class="section">
<h5 class="section-heading">Toggles</h5>
<h5 class="section-heading">Options</h5>
<div class="gf-form-group">
<div class="gf-form">
<label class="gf-form-label width-11">Timezone</label>
<div class="gf-form-select-wrapper">
<select ng-model="dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
</div>
</div>
<gf-form-switch class="gf-form"
label="Editable"
tooltip="Uncheck, then save and reload to disable all dashboard editing"
@ -116,28 +121,7 @@
</div>
<div ng-if="editor.index == 4">
<h5 class="section-heading">Dashboard info</h5>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-10">Last updated at:</span>
<span class="gf-form-label width-18">{{dashboard.formatDate(dashboardMeta.updated)}}</span>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Last updated by:</span>
<span class="gf-form-label width-18">{{dashboardMeta.updatedBy}}&nbsp;</span>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Created at:</span>
<span class="gf-form-label width-18">{{dashboard.formatDate(dashboardMeta.created)}}&nbsp;</span>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Created by:</span>
<span class="gf-form-label width-18">{{dashboardMeta.createdBy}}&nbsp;</span>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Current version:</span>
<span class="gf-form-label width-18">{{dashboardMeta.version}}&nbsp;</span>
</div>
</div>
<acl-settings dashboard="dashboard"></acl-settings>
</div>
</div>

View File

@ -18,9 +18,15 @@ const template = `
<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content" novalidate>
<div class="p-t-2">
<div class="gf-form">
<label class="gf-form-label">New name</label>
<label class="gf-form-label width-7">New name</label>
<input type="text" class="gf-form-input" ng-model="ctrl.clone.title" give-focus="true" required>
</div>
<div class="gf-form">
<folder-picker initial-title="ctrl.folderTitle"
on-change="ctrl.onFolderChange($folder)"
label-class="width-7">
</folder-picker>
</div>
</div>
<div class="gf-form-button-row text-center">
@ -33,6 +39,7 @@ const template = `
export class SaveDashboardAsModalCtrl {
clone: any;
folderTitle: any;
dismiss: () => void;
/** @ngInject */
@ -43,6 +50,7 @@ export class SaveDashboardAsModalCtrl {
this.clone.title += ' Copy';
this.clone.editable = true;
this.clone.hideControls = false;
this.folderTitle = dashboard.meta.folderTitle || 'Root';
// remove alerts
this.clone.rows.forEach(row => {
@ -63,6 +71,10 @@ export class SaveDashboardAsModalCtrl {
this.save();
}
}
onFolderChange(folder) {
this.clone.folderId = folder.id;
}
}
export function saveDashboardAsDirective() {

View File

@ -1,7 +1,6 @@
define([
'./org_users_ctrl',
'./profile_ctrl',
'./org_users_ctrl',
'./select_org_ctrl',
'./change_password_ctrl',
'./newOrgCtrl',
@ -9,4 +8,7 @@ define([
'./orgApiKeysCtrl',
'./orgDetailsCtrl',
'./prefs_control',
'./user_groups_ctrl',
'./user_group_details_ctrl',
'./create_user_group_modal',
], function () {});

View File

@ -0,0 +1,38 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
export class CreateUserGroupCtrl {
userGroupName = '';
/** @ngInject */
constructor(private backendSrv, private $scope, $sce, private $location) {
}
createUserGroup() {
this.backendSrv.post('/api/user-groups', {name: this.userGroupName}).then((result) => {
if (result.userGroupId) {
this.$location.path('/org/user-groups/edit/' + result.userGroupId);
}
this.dismiss();
});
}
dismiss() {
appEvents.emit('hide-modal');
}
}
export function createUserGroupModal() {
return {
restrict: 'E',
templateUrl: 'public/app/features/org/partials/create_user_group.html',
controller: CreateUserGroupCtrl,
bindToController: true,
controllerAs: 'ctrl',
};
}
coreModule.directive('createUserGroupModal', createUserGroupModal);

View File

@ -0,0 +1,26 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<span class="p-l-1">Create User Group</span>
</h2>
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content">
<form name="ctrl.createUserGroupForm" class="gf-form-group" novalidate>
<div class="p-t-2">
<div class="gf-form-inline">
<div class="gf-form max-width-21">
<input type="text" class="gf-form-input" ng-model='ctrl.userGroupName' required give-focus="true" placeholder="Enter User Group Name"></input>
</div>
<div class="gf-form">
<button class="btn gf-form-btn btn-success" ng-click="ctrl.createUserGroup();ctrl.dismiss();">Create</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,49 @@
<navbar model="ctrl.navModel"></navbar>
<div class="page-container">
<div class="page-header">
<h1>Edit User Group</h1>
</div>
<form name="userGroupDetailsForm" class="gf-form-group gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10">Name</span>
<input type="text" required ng-model="ctrl.userGroup.name" class="gf-form-input max-width-14" >
</div>
<div class="gf-form">
<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Update</button>
</div>
</form>
<div class="gf-form-group">
<h3 class="page-heading">User Group Members</h3>
<form name="ctrl.addMemberForm" class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-10">User</span>
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
</div>
</form>
<table class="grafana-options-table" ng-show="ctrl.userGroupMembers.length > 0">
<tr>
<th>Username</th>
<th>Email</th>
<th></th>
</tr>
<tr ng-repeat="member in ctrl.userGroupMembers">
<td>{{member.login}}</td>
<td>{{member.email}}</td>
<td style="width: 1%">
<a ng-click="ctrl.removeUserGroupMember(member)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</table>
<div>
<em class="muted" ng-hide="ctrl.userGroupMembers.length > 0">
This user group has no members yet.
</em>
</div>

View File

@ -0,0 +1,61 @@
<navbar model="ctrl.navModel"></navbar>
<div class="page-container">
<div class="page-header">
<h1>User Groups</h1>
<a class="btn btn-success" ng-click="ctrl.openUserGroupModal()">
<i class="fa fa-plus"></i>
Create User Group
</a>
</div>
<div class="gf-form pull-right width-15 gf-form-group">
<span style="position: relative;">
<input type="text" class="gf-form-input" placeholder="Find User Group by name" tabindex="1" give-focus="true"
ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.get()" />
</span>
</div>
<div class="admin-list-table">
<table class="filter-table form-inline" ng-show="ctrl.userGroups.length > 0">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="userGroup in ctrl.userGroups">
<td>{{userGroup.id}}</td>
<td>{{userGroup.name}}</td>
<td class="text-right">
<a href="org/user-groups/edit/{{userGroup.id}}" class="btn btn-inverse btn-small">
<i class="fa fa-edit"></i>
Edit
</a>
&nbsp;&nbsp;
<a ng-click="ctrl.deleteUserGroup(userGroup)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="admin-list-paging" ng-if="ctrl.showPaging">
<ol>
<li ng-repeat="page in ctrl.pages">
<button
class="btn btn-small"
ng-class="{'btn-secondary': page.current, 'btn-inverse': !page.current}"
ng-click="ctrl.navigateToPage(page)">{{page.page}}</button>
</li>
</ol>
</div>
<em class="muted" ng-hide="ctrl.userGroups.length > 0">
No User Groups found.
</em>
</div>

View File

@ -0,0 +1,45 @@
import '../user_group_details_ctrl';
import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common';
import UserGroupDetailsCtrl from '../user_group_details_ctrl';
describe('UserGroupDetailsCtrl', () => {
var ctx: any = {};
var backendSrv = {
searchUsers: sinon.stub().returns(Promise.resolve([])),
get: sinon.stub().returns(Promise.resolve([])),
post: sinon.stub().returns(Promise.resolve([]))
};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
ctx.ctrl = $controller(UserGroupDetailsCtrl, {
$scope: ctx.scope,
backendSrv: backendSrv,
$routeParams: {id: 1}
});
}));
describe('when user is chosen to be added to user group', () => {
beforeEach(() => {
const userItem = {
id: 2,
login: 'user2',
};
ctx.ctrl.userPicked(userItem);
});
it('should parse the result and save to db', () => {
expect(backendSrv.post.getCall(0).args[0]).to.eql('/api/user-groups/1/members');
expect(backendSrv.post.getCall(0).args[1].userId).to.eql(2);
});
it('should refresh the list after saving.', () => {
expect(backendSrv.get.getCall(0).args[0]).to.eql('/api/user-groups/1');
expect(backendSrv.get.getCall(1).args[0]).to.eql('/api/user-groups/1/members');
});
});
});

View File

@ -0,0 +1,78 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import _ from 'lodash';
export default class UserGroupDetailsCtrl {
userGroup: UserGroup;
userGroupMembers: User[] = [];
navModel: any;
constructor(private $scope, private $http, private backendSrv, private $routeParams, navModelSrv) {
this.navModel = navModelSrv.getOrgNav(3);
this.get();
}
get() {
if (this.$routeParams && this.$routeParams.id) {
this.backendSrv.get(`/api/user-groups/${this.$routeParams.id}`)
.then(result => {
this.userGroup = result;
});
this.backendSrv.get(`/api/user-groups/${this.$routeParams.id}/members`)
.then(result => {
this.userGroupMembers = result;
});
}
}
removeUserGroupMember(userGroupMember: UserGroupMember) {
this.$scope.appEvent('confirm-modal', {
title: 'Remove Member',
text: 'Are you sure you want to remove ' + userGroupMember.name + ' from this group?',
yesText: "Remove",
icon: "fa-warning",
onConfirm: () => {
this.removeMemberConfirmed(userGroupMember);
}
});
}
removeMemberConfirmed(userGroupMember: UserGroupMember) {
this.backendSrv.delete(`/api/user-groups/${this.$routeParams.id}/members/${userGroupMember.userId}`)
.then(this.get.bind(this));
}
update() {
if (!this.$scope.userGroupDetailsForm.$valid) { return; }
this.backendSrv.put('/api/user-groups/' + this.userGroup.id, {name: this.userGroup.name});
}
userPicked(user) {
this.backendSrv.post(`/api/user-groups/${this.$routeParams.id}/members`, {userId: user.id}).then(() => {
this.$scope.$broadcast('user-picker-reset');
this.get();
});
}
}
export interface UserGroup {
id: number;
name: string;
}
export interface User {
id: number;
name: string;
login: string;
email: string;
}
export interface UserGroupMember {
userId: number;
name: string;
}
coreModule.controller('UserGroupDetailsCtrl', UserGroupDetailsCtrl);

View File

@ -0,0 +1,68 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import {appEvents} from 'app/core/core';
export class UserGroupsCtrl {
userGroups: any;
pages = [];
perPage = 50;
page = 1;
totalPages: number;
showPaging = false;
query: any = '';
navModel: any;
/** @ngInject */
constructor(private $scope, private $http, private backendSrv, private $location, navModelSrv) {
this.navModel = navModelSrv.getOrgNav(3);
this.get();
}
get() {
this.backendSrv.get(`/api/user-groups/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
.then((result) => {
this.userGroups = result.userGroups;
this.page = result.page;
this.perPage = result.perPage;
this.totalPages = Math.ceil(result.totalCount / result.perPage);
this.showPaging = this.totalPages > 1;
this.pages = [];
for (var i = 1; i < this.totalPages+1; i++) {
this.pages.push({ page: i, current: i === this.page});
}
});
}
navigateToPage(page) {
this.page = page.page;
this.get();
}
deleteUserGroup(userGroup) {
this.$scope.appEvent('confirm-modal', {
title: 'Delete',
text: 'Are you sure you want to delete User Group ' + userGroup.name + '?',
yesText: "Delete",
icon: "fa-warning",
onConfirm: () => {
this.deleteUserGroupConfirmed(userGroup);
}
});
}
deleteUserGroupConfirmed(userGroup) {
this.backendSrv.delete('/api/user-groups/' + userGroup.id)
.then(this.get.bind(this));
}
openUserGroupModal() {
appEvents.emit('show-modal', {
templateHtml: '<create-user-group-modal></create-user-group-modal>',
modalClass: 'modal--narrow'
});
}
}
coreModule.controller('UserGroupsCtrl', UserGroupsCtrl);

View File

@ -10,7 +10,7 @@ var template = `
<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>
<span class="panel-menu-container dropdown">
<span class="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown"></span>
<ul class="dropdown-menu panel-menu" role="menu">
<ul class="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
<li>
<a ng-click="ctrl.addDataQuery(datasource);">
<i class="fa fa-cog"></i> Edit <span class="dropdown-menu-item-shortcut">e</span>
@ -45,7 +45,7 @@ function renderMenuItem(item, ctrl) {
if (item.href) { html += ` href="${item.href}"`; }
html += `><i class="${item.icon}"></i>`;
html += `<span>${item.text}</span>`;
html += `<span class="dropdown-item-text">${item.text}</span>`;
if (item.shortcut) {
html += `<span class="dropdown-menu-item-shortcut">${item.shortcut}</span>`;
@ -54,7 +54,7 @@ function renderMenuItem(item, ctrl) {
html += `</a>`;
if (item.submenu) {
html += '<ul class="dropdown-menu panel-menu">';
html += '<ul class="dropdown-menu dropdown-menu--menu panel-menu">';
for (let subitem of item.submenu) {
html += renderMenuItem(subitem, ctrl);
}

View File

@ -22,6 +22,14 @@
<input type="text" class="gf-form-input" placeholder="title query" ng-model="ctrl.panel.query" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<folder-picker root-name="All"
initial-folder-id="ctrl.panel.folderId"
on-change="ctrl.onFolderChange($folder)"
label-class="width-6">
</folder-picker>
</div>
<div class="gf-form">
<span class="gf-form-label width-6">Tags</span>
<bootstrap-tagsinput ng-model="ctrl.panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">

View File

@ -19,6 +19,7 @@ class DashListCtrl extends PanelCtrl {
search: false,
starred: true,
headings: true,
folderId: 0,
};
/** @ngInject */
@ -87,6 +88,7 @@ class DashListCtrl extends PanelCtrl {
limit: this.panel.limit,
query: this.panel.query,
tag: this.panel.tags,
folderId: this.panel.folderId
};
return this.backendSrv.search(params).then(result => {
@ -123,6 +125,11 @@ class DashListCtrl extends PanelCtrl {
});
});
}
onFolderChange(folder: any) {
this.panel.folderId = folder.id;
this.refresh();
}
}
export {DashListCtrl, DashListCtrl as PanelCtrl};

View File

@ -0,0 +1,13 @@
<div>
<div class="section gf-form-group">
<h5 class="section-heading">Options</h5>
<div class="gf-form">
<folder-picker root-name="All"
initial-folder-id="ctrl.panel.folderId"
on-change="ctrl.onFolderChange($folder)"
label-class="width-6">
</folder-picker>
</div>
</div>
</div>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100px" height="100px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<g>
<path fill="#1F1F1F" d="M18.2,4.2c-1.5,0-2.9,0.6-3.9,1.6l-0.5,0.5l-0.5-0.5c-1-1.1-2.4-1.6-3.9-1.6S6.6,4.8,5.5,5.8
c-1,1-1.6,2.4-1.6,3.9s0.6,2.9,1.6,3.9l8.3,8.3l8.3-8.3c1-1,1.6-2.4,1.6-3.9s-0.6-2.9-1.6-3.9C21.1,4.8,19.7,4.2,18.2,4.2z
M21,12.5l-7.2,7.2l-7.1-7.1c-0.7-0.7-1.1-1.7-1.1-2.8c0-1,0.4-2,1.1-2.8c0.7-0.7,1.7-1.1,2.8-1.1c1,0,2,0.4,2.8,1.1l0.5,0.5
l0.6,0.6l0.6,0.6l0.6-0.6L15,7.5L15.5,7c0.7-0.7,1.7-1.1,2.8-1.1c1,0,2,0.4,2.8,1.1c0.7,0.7,1.1,1.7,1.1,2.8
C22.1,10.8,21.7,11.8,21,12.5z"/>
<path fill="#1F1F1F" d="M18.2,77.3c-1.5,0-2.9,0.6-3.9,1.6l-0.5,0.5L13.4,79c-1-1.1-2.4-1.6-3.9-1.6S6.6,77.9,5.5,79
c-1,1-1.6,2.4-1.6,3.9s0.6,2.9,1.6,3.9l8.3,8.3l8.3-8.3c1-1,1.6-2.4,1.6-3.9S23.2,80,22.1,79C21.1,77.9,19.7,77.3,18.2,77.3z
M21,85.6l-7.2,7.2l-7.1-7.1c-0.7-0.7-1.1-1.7-1.1-2.8c0-1,0.4-2,1.1-2.8C7.4,79.4,8.4,79,9.4,79c1,0,2,0.4,2.8,1.1l0.5,0.5
l0.6,0.6l0.6,0.6l0.6-0.6l0.6-0.6l0.5-0.5c0.7-0.7,1.7-1.1,2.8-1.1c1,0,2,0.4,2.8,1.1c0.7,0.7,1.1,1.7,1.1,2.8
C22.1,83.9,21.7,84.9,21,85.6z"/>
<path fill="#1F1F1F" d="M0,0v100h100V0H0z M22.7,87.4l-8.9,8.9l-8.9-8.9C3.7,86.2,3,84.6,3,82.9s0.7-3.3,1.9-4.5
c1.2-1.2,2.8-1.9,4.5-1.9c1.6,0,3.2,0.6,4.4,1.7c1.2-1.1,2.7-1.7,4.4-1.7c1.7,0,3.3,0.7,4.5,1.9c1.2,1.2,1.9,2.8,1.9,4.5
S23.9,86.2,22.7,87.4z M22.7,63l-8.9,8.9L4.9,63C3.7,61.8,3,60.2,3,58.5c0-1.7,0.7-3.3,1.9-4.5c1.2-1.2,2.8-1.9,4.5-1.9
c1.1,0,2.1,0.3,3,0.7l0,0.1l0,0l-1.3,6.6l0,0l0,0.1l2.3-0.1l-0.6,6.1l0-0.1l0,0l3.4-8.3l-2,0.3l1.2-3.7l0-0.1l0,0l0.3-1.1
c0.8-0.3,1.6-0.5,2.4-0.5c1.7,0,3.3,0.7,4.5,1.9c1.2,1.2,1.9,2.8,1.9,4.5C24.6,60.2,23.9,61.8,22.7,63z M22.7,38.7l-8.9,8.9
l-8.9-8.9C3.7,37.5,3,35.9,3,34.1s0.7-3.3,1.9-4.5c1.2-1.2,2.8-1.9,4.5-1.9c1.1,0,2.1,0.3,3,0.7l0,0.1l0,0l-1.3,6.6l0,0l0,0.1
l2.3-0.1l-0.6,6.1l0-0.1l0,0l3.4-8.3l-2,0.3l1.2-3.7l0-0.1l0,0l0.3-1.1c0.8-0.3,1.6-0.5,2.4-0.5c1.7,0,3.3,0.7,4.5,1.9
c1.2,1.2,1.9,2.8,1.9,4.5S23.9,37.5,22.7,38.7z M22.7,14.3l-8.9,8.9l-8.9-8.9C3.7,13.1,3,11.5,3,9.8s0.7-3.3,1.9-4.5
c1.2-1.2,2.8-1.9,4.5-1.9c1.6,0,3.2,0.6,4.4,1.7C15,4,16.6,3.4,18.2,3.4c1.7,0,3.3,0.7,4.5,1.9c1.2,1.2,1.9,2.8,1.9,4.5
S23.9,13.1,22.7,14.3z M96.6,94.4c0,1-0.8,1.8-1.8,1.8H33.4c-1,0-1.8-0.8-1.8-1.8V78.6c0-1,0.8-1.8,1.8-1.8h61.4
c1,0,1.8,0.8,1.8,1.8V94.4z M96.6,69.9c0,1-0.8,1.8-1.8,1.8H33.4c-1,0-1.8-0.8-1.8-1.8V54.1c0-1,0.8-1.8,1.8-1.8h61.4
c1,0,1.8,0.8,1.8,1.8V69.9z M96.6,45.4c0,1-0.8,1.8-1.8,1.8H33.4c-1,0-1.8-0.8-1.8-1.8V29.6c0-1,0.8-1.8,1.8-1.8h61.4
c1,0,1.8,0.8,1.8,1.8V45.4z M96.6,20.9c0,1-0.8,1.8-1.8,1.8H33.4c-1,0-1.8-0.8-1.8-1.8V5.1c0-1,0.8-1.8,1.8-1.8h61.4
c1,0,1.8,0.8,1.8,1.8V20.9z"/>
<path fill="#1F1F1F" d="M18.2,53c-0.5,0-1,0.1-1.4,0.2l-0.7,2.1c0.6-0.4,1.3-0.6,2.1-0.6c1,0,2,0.4,2.8,1.1
c0.7,0.7,1.1,1.7,1.1,2.8c0,1-0.4,2-1.1,2.8l-7.2,7.2l-7.1-7.1c-0.7-0.7-1.1-1.7-1.1-2.8c0-1,0.4-2,1.1-2.8
c0.7-0.7,1.7-1.1,2.8-1.1c0.5,0,1,0.1,1.5,0.3l0.3-1.6c-0.6-0.2-1.2-0.3-1.8-0.3c-1.5,0-2.9,0.6-3.9,1.6c-1,1-1.6,2.4-1.6,3.9
c0,1.5,0.6,2.9,1.6,3.9l8.3,8.3l8.3-8.3c1-1,1.6-2.4,1.6-3.9c0-1.5-0.6-2.9-1.6-3.9C21.1,53.5,19.7,53,18.2,53z"/>
<path fill="#1F1F1F" d="M18.2,28.6c-0.5,0-1,0.1-1.4,0.2l-0.7,2.1c0.6-0.4,1.3-0.6,2.1-0.6c1,0,2,0.4,2.8,1.1
c0.7,0.7,1.1,1.7,1.1,2.8c0,1-0.4,2-1.1,2.8l-7.2,7.2l-7.1-7.1c-0.7-0.7-1.1-1.7-1.1-2.8c0-1,0.4-2,1.1-2.8
c0.7-0.7,1.7-1.1,2.8-1.1c0.5,0,1,0.1,1.5,0.3l0.3-1.6c-0.6-0.2-1.2-0.3-1.8-0.3c-1.5,0-2.9,0.6-3.9,1.6c-1,1-1.6,2.4-1.6,3.9
s0.6,2.9,1.6,3.9l8.3,8.3l8.3-8.3c1-1,1.6-2.4,1.6-3.9s-0.6-2.9-1.6-3.9C21.1,29.2,19.7,28.6,18.2,28.6z"/>
<path fill="#898989" d="M94.7,3.3H33.4c-1,0-1.8,0.8-1.8,1.8v15.8c0,1,0.8,1.8,1.8,1.8h61.4c1,0,1.8-0.8,1.8-1.8V5.1
C96.6,4.1,95.7,3.3,94.7,3.3z"/>
<path fill="#898989" d="M94.7,27.8H33.4c-1,0-1.8,0.8-1.8,1.8v15.8c0,1,0.8,1.8,1.8,1.8h61.4c1,0,1.8-0.8,1.8-1.8V29.6
C96.6,28.6,95.7,27.8,94.7,27.8z"/>
<path fill="#898989" d="M94.7,52.3H33.4c-1,0-1.8,0.8-1.8,1.8v15.8c0,1,0.8,1.8,1.8,1.8h61.4c1,0,1.8-0.8,1.8-1.8V54.1
C96.6,53.1,95.7,52.3,94.7,52.3z"/>
<path fill="#898989" d="M94.7,76.8H33.4c-1,0-1.8,0.8-1.8,1.8v15.8c0,1,0.8,1.8,1.8,1.8h61.4c1,0,1.8-0.8,1.8-1.8V78.6
C96.6,77.6,95.7,76.8,94.7,76.8z"/>
<path fill="#04A64D" d="M18.2,3.4c-1.6,0-3.2,0.6-4.4,1.7C12.6,4,11.1,3.4,9.4,3.4c-1.7,0-3.3,0.7-4.5,1.9C3.7,6.5,3,8.1,3,9.8
s0.7,3.3,1.9,4.5l8.9,8.9l8.9-8.9c1.2-1.2,1.9-2.8,1.9-4.5s-0.7-3.3-1.9-4.5C21.5,4.1,19.9,3.4,18.2,3.4z M22.1,13.7L13.8,22
l-8.3-8.3c-1-1-1.6-2.4-1.6-3.9s0.6-2.9,1.6-3.9c1-1,2.4-1.6,3.9-1.6s2.9,0.6,3.9,1.6l0.5,0.5l0.5-0.5c1-1,2.4-1.6,3.9-1.6
c1.5,0,2.9,0.6,3.9,1.6c1,1,1.6,2.4,1.6,3.9S23.2,12.7,22.1,13.7z"/>
<path fill="#04A64D" d="M18.2,5.9c-1,0-2,0.4-2.8,1.1L15,7.5l-0.6,0.6l-0.6,0.6l-0.6-0.6l-0.6-0.6L12.2,7c-0.7-0.7-1.7-1.1-2.8-1.1
c-1,0-2,0.4-2.8,1.1C5.9,7.8,5.5,8.7,5.5,9.8c0,1,0.4,2,1.1,2.8l7.1,7.1l7.2-7.2c0.7-0.7,1.1-1.7,1.1-2.8c0-1-0.4-2-1.1-2.8
C20.2,6.3,19.3,5.9,18.2,5.9z"/>
<path fill="#04A64D" d="M18.2,76.5c-1.6,0-3.2,0.6-4.4,1.7c-1.2-1.1-2.7-1.7-4.4-1.7c-1.7,0-3.3,0.7-4.5,1.9
C3.7,79.6,3,81.2,3,82.9s0.7,3.3,1.9,4.5l8.9,8.9l8.9-8.9c1.2-1.2,1.9-2.8,1.9-4.5s-0.7-3.3-1.9-4.5C21.5,77.2,19.9,76.5,18.2,76.5
z M22.1,86.8l-8.3,8.3l-8.3-8.3c-1-1-1.6-2.4-1.6-3.9S4.5,80,5.5,79c1-1,2.4-1.6,3.9-1.6s2.9,0.6,3.9,1.6l0.5,0.5l0.5-0.5
c1-1,2.4-1.6,3.9-1.6c1.5,0,2.9,0.6,3.9,1.6c1,1,1.6,2.4,1.6,3.9S23.2,85.8,22.1,86.8z"/>
<path fill="#04A64D" d="M18.2,79c-1,0-2,0.4-2.8,1.1L15,80.6l-0.6,0.6l-0.6,0.6l-0.6-0.6l-0.6-0.6l-0.5-0.5
c-0.7-0.7-1.7-1.1-2.8-1.1c-1,0-2,0.4-2.8,1.1c-0.7,0.7-1.1,1.7-1.1,2.8c0,1,0.4,2,1.1,2.8l7.1,7.1l7.2-7.2
c0.7-0.7,1.1-1.7,1.1-2.8c0-1-0.4-2-1.1-2.8C20.2,79.4,19.3,79,18.2,79z"/>
<path fill="#EB242A" d="M18.2,27.8c-0.8,0-1.7,0.2-2.4,0.5l-0.3,1.1l0,0l0,0.1l-1.2,3.7l2-0.3l-3.4,8.3l0,0l0,0.1l0.6-6.1l-2.3,0.1
l0-0.1l0,0l1.3-6.6l0,0l0-0.1c-0.9-0.5-1.9-0.7-3-0.7c-1.7,0-3.3,0.7-4.5,1.9C3.7,30.8,3,32.4,3,34.1s0.7,3.3,1.9,4.5l8.9,8.9
l8.9-8.9c1.2-1.2,1.9-2.8,1.9-4.5s-0.7-3.3-1.9-4.5C21.5,28.4,19.9,27.8,18.2,27.8z M22.1,38.1l-8.3,8.3l-8.3-8.3
c-1-1-1.6-2.4-1.6-3.9s0.6-2.9,1.6-3.9c1-1,2.4-1.6,3.9-1.6c0.6,0,1.2,0.1,1.8,0.3l-0.3,1.6c-0.5-0.2-1-0.3-1.5-0.3
c-1,0-2,0.4-2.8,1.1c-0.7,0.7-1.1,1.7-1.1,2.8c0,1,0.4,2,1.1,2.8l7.1,7.1l7.2-7.2c0.7-0.7,1.1-1.7,1.1-2.8c0-1-0.4-2-1.1-2.8
c-0.7-0.7-1.7-1.1-2.8-1.1c-0.8,0-1.5,0.2-2.1,0.6l0.7-2.1c0.5-0.1,0.9-0.2,1.4-0.2c1.5,0,2.9,0.6,3.9,1.6c1,1,1.6,2.4,1.6,3.9
S23.2,37,22.1,38.1z"/>
<path fill="#EB242A" d="M18.2,52.1c-0.8,0-1.7,0.2-2.4,0.5l-0.3,1.1l0,0l0,0.1l-1.2,3.7l2-0.3l-3.4,8.3l0,0l0,0.1l0.6-6.1l-2.3,0.1
l0-0.1l0,0l1.3-6.6l0,0l0-0.1c-0.9-0.5-1.9-0.7-3-0.7c-1.7,0-3.3,0.7-4.5,1.9C3.7,55.2,3,56.8,3,58.5c0,1.7,0.7,3.3,1.9,4.5
l8.9,8.9l8.9-8.9c1.2-1.2,1.9-2.8,1.9-4.5c0-1.7-0.7-3.3-1.9-4.5C21.5,52.8,19.9,52.1,18.2,52.1z M22.1,62.4l-8.3,8.3l-8.3-8.3
c-1-1-1.6-2.4-1.6-3.9c0-1.5,0.6-2.9,1.6-3.9c1-1,2.4-1.6,3.9-1.6c0.6,0,1.2,0.1,1.8,0.3l-0.3,1.6c-0.5-0.2-1-0.3-1.5-0.3
c-1,0-2,0.4-2.8,1.1c-0.7,0.7-1.1,1.7-1.1,2.8c0,1,0.4,2,1.1,2.8l7.1,7.1l7.2-7.2c0.7-0.7,1.1-1.7,1.1-2.8c0-1-0.4-2-1.1-2.8
c-0.7-0.7-1.7-1.1-2.8-1.1c-0.8,0-1.5,0.2-2.1,0.6l0.7-2.1c0.5-0.1,0.9-0.2,1.4-0.2c1.5,0,2.9,0.6,3.9,1.6c1,1,1.6,2.4,1.6,3.9
C23.8,60,23.2,61.4,22.1,62.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,30 @@
<section class="card-section card-list-layout-list">
<ol class="card-list">
<li class="card-item-wrapper" ng-repeat="permission in ctrl.userPermissions">
<div class="card-item card-item--alert">
<div class="card-item-header">
<div class="card-item-sub-name">{{permission.permissionName}}</div>
</div>
<div class="card-item-body">
<div class="card-item-details">
<div class="card-item-notice">{{permission.userLogin}}</div>
</div>
</div>
</div>
</div>
</li>
<li class="card-item-wrapper" ng-repeat="permission in ctrl.userGroupPermissions">
<div class="card-item card-item--alert">
<div class="card-item-header">
<div class="card-item-sub-name">{{permission.permissionName}}</div>
</div>
<div class="card-item-body">
<div class="card-item-details">
<div class="card-item-notice">{{permission.userGroup}}</div>
</div>
</div>
</div>
</div>
</li>
</ol>
</section>

View File

@ -0,0 +1,60 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import config from 'app/core/config';
import {PanelCtrl} from 'app/plugins/sdk';
import {impressions} from 'app/features/dashboard/impression_store';
class PermissionListCtrl extends PanelCtrl {
static templateUrl = 'module.html';
userPermissions: any[];
userGroupPermissions: any[];
roles: any[];
panelDefaults = {
folderId: 0
};
/** @ngInject */
constructor($scope, $injector, private backendSrv) {
super($scope, $injector);
_.defaults(this.panel, this.panelDefaults);
this.events.on('refresh', this.onRefresh.bind(this));
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
this.getPermissions();
}
onInitEditMode() {
this.editorTabIndex = 1;
this.addEditorTab('Options', 'public/app/plugins/panel/permissionlist/editor.html');
}
onRefresh() {
var promises = [];
promises.push(this.getPermissions());
return Promise.all(promises)
.then(this.renderingCompleted.bind(this));
}
onFolderChange(folder: any) {
this.panel.folderId = folder.id;
this.refresh();
}
getPermissions() {
return this.backendSrv.get(`/api/dashboards/id/${this.panel.folderId}/acl`)
.then(result => {
this.userPermissions = _.filter(result, p => { return p.userId > 0;});
this.userGroupPermissions = _.filter(result, p => { return p.userGroupId > 0;});
// this.roles = this.setRoles(result);
});
}
}
export {PermissionListCtrl, PermissionListCtrl as PanelCtrl};

Some files were not shown because too many files have changed in this diff Show More