Commit aab6c98e by Torkel Ödegaard

Merge branch 'dashboard-acl' into dashboard_folders

parents 1e064f6c a37c4411
......@@ -23,7 +23,8 @@ func GetDashboardAclList(c *middleware.Context) Response {
return ApiError(500, "Failed to get Dashboard ACL", err)
}
return Json(200, &query.Result)
list := query.Result
return Json(200, list)
}
func PostDashboardAcl(c *middleware.Context, cmd m.SetDashboardAclCommand) Response {
......
......@@ -13,16 +13,16 @@ import (
func TestDashboardAclApiEndpoint(t *testing.T) {
Convey("Given a dashboard acl", t, func() {
mockResult := []*models.DashboardAcl{
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permissions: models.PERMISSION_EDIT},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permissions: models.PERMISSION_VIEW},
{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permissions: models.PERMISSION_EDIT},
{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permissions: models.PERMISSION_READ_ONLY_EDIT},
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: models.PERMISSION_EDIT},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: models.PERMISSION_VIEW},
{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permission: models.PERMISSION_EDIT},
{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permission: models.PERMISSION_READ_ONLY_EDIT},
}
dtoRes := []*models.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permissions: models.PERMISSION_EDIT},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permissions: models.PERMISSION_VIEW},
{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permissions: models.PERMISSION_EDIT},
{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permissions: models.PERMISSION_READ_ONLY_EDIT},
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: models.PERMISSION_EDIT},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: models.PERMISSION_VIEW},
{Id: 3, OrgId: 1, DashboardId: 1, UserGroupId: 1, Permission: models.PERMISSION_EDIT},
{Id: 4, OrgId: 1, DashboardId: 1, UserGroupId: 2, Permission: models.PERMISSION_READ_ONLY_EDIT},
}
bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error {
......@@ -59,7 +59,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
Convey("When user is editor and in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_EDIT})
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: models.PERMISSION_EDIT})
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
......@@ -70,7 +70,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_EDIT})
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: models.PERMISSION_EDIT})
bus.AddHandler("test3", func(cmd *models.RemoveDashboardAclCommand) error {
return nil
......@@ -114,7 +114,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", models.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permissions: models.PERMISSION_VIEW})
mockResult = append(mockResult, &models.DashboardAcl{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: models.PERMISSION_VIEW})
bus.AddHandler("test3", func(cmd *models.RemoveDashboardAclCommand) error {
return nil
})
......
......@@ -174,7 +174,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
aclMockResp := []*models.DashboardAcl{
{
DashboardId: 1,
Permissions: models.PERMISSION_EDIT,
Permission: models.PERMISSION_EDIT,
UserId: 200,
},
}
......@@ -273,7 +273,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
role := models.ROLE_VIEWER
mockResult := []*models.DashboardAcl{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permissions: models.PERMISSION_EDIT},
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *models.GetInheritedDashboardAclQuery) error {
......@@ -315,7 +315,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
role := models.ROLE_EDITOR
mockResult := []*models.DashboardAcl{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permissions: models.PERMISSION_VIEW},
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *models.GetInheritedDashboardAclQuery) error {
......
......@@ -9,15 +9,15 @@ type PermissionType int
const (
PERMISSION_VIEW PermissionType = 1 << iota
PERMISSION_READ_ONLY_EDIT
PERMISSION_EDIT
PERMISSION_ADMIN
)
func (p PermissionType) String() string {
names := map[int]string{
int(PERMISSION_VIEW): "View",
int(PERMISSION_READ_ONLY_EDIT): "Read-only Edit",
int(PERMISSION_EDIT): "Edit",
int(PERMISSION_VIEW): "View",
int(PERMISSION_EDIT): "Edit",
int(PERMISSION_ADMIN): "Admin",
}
return names[int(p)]
}
......@@ -36,7 +36,7 @@ type DashboardAcl struct {
UserId int64
UserGroupId int64
Permissions PermissionType
Permission PermissionType
Created time.Time
Updated time.Time
......@@ -55,7 +55,8 @@ type DashboardAclInfoDTO struct {
UserEmail string `json:"userEmail"`
UserGroupId int64 `json:"userGroupId"`
UserGroup string `json:"userGroup"`
Permissions PermissionType `json:"permissions"`
Role RoleType `json:"role"`
Permission PermissionType `json:"permission"`
PermissionName string `json:"permissionName"`
}
......@@ -68,7 +69,7 @@ type SetDashboardAclCommand struct {
OrgId int64 `json:"-"`
UserId int64 `json:"userId"`
UserGroupId int64 `json:"userGroupId"`
Permissions PermissionType `json:"permissions" binding:"Required"`
Permission PermissionType `json:"permission" binding:"Required"`
Result DashboardAcl `json:"-"`
}
......
......@@ -29,7 +29,7 @@ func (g *DashboardGuardian) CanSave() (bool, error) {
}
func (g *DashboardGuardian) CanEdit() (bool, error) {
return g.HasPermission(m.PERMISSION_READ_ONLY_EDIT, m.ROLE_READ_ONLY_EDITOR)
return g.HasPermission(m.PERMISSION_EDIT, m.ROLE_READ_ONLY_EDITOR)
}
func (g *DashboardGuardian) CanView() (bool, error) {
......@@ -57,12 +57,12 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType, fallbackR
}
for _, p := range acl {
if p.UserId == g.user.UserId && p.Permissions >= permission {
if p.UserId == g.user.UserId && p.Permission >= permission {
return true, nil
}
for _, ug := range userGroups {
if ug.Id == p.UserGroupId && p.Permissions >= permission {
if ug.Id == p.UserGroupId && p.Permission >= permission {
return true, nil
}
}
......
......@@ -27,11 +27,13 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
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{
Permissions: cmd.Permissions,
Updated: time.Now(),
Permission: cmd.Permission,
Updated: time.Now(),
}
if _, err := sess.Cols("updated", "permissions").Where("dashboard_id =? and (user_group_id=? or user_id=?)", cmd.DashboardId, cmd.UserGroupId, cmd.UserId).Update(&entity); err != nil {
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
}
......@@ -45,10 +47,10 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
Created: time.Now(),
Updated: time.Now(),
DashboardId: cmd.DashboardId,
Permissions: cmd.Permissions,
Permission: cmd.Permission,
}
cols := []string{"org_id", "created", "updated", "dashboard_id", "permissions"}
cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"}
if cmd.UserId != 0 {
cols = append(cols, "user_id")
......@@ -58,12 +60,12 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
cols = append(cols, "user_group_id")
}
entityId, err := sess.Cols(cols...).Insert(&entity)
_, err := sess.Cols(cols...).Insert(&entity)
if err != nil {
return err
}
cmd.Result = entity
cmd.Result.Id = entityId
// Update dashboard HasAcl flag
dashboard := m.Dashboard{
......@@ -97,7 +99,7 @@ func GetInheritedDashboardAcl(query *m.GetInheritedDashboardAclQuery) error {
da.dashboard_id,
da.user_id,
da.user_group_id,
da.permissions,
da.permission,
da.created,
da.updated
FROM dashboard_acl as da
......@@ -112,29 +114,51 @@ func GetInheritedDashboardAcl(query *m.GetInheritedDashboardAclQuery) error {
}
func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
rawSQL := `SELECT
da.id,
da.org_id,
da.dashboard_id,
da.user_id,
da.user_group_id,
da.permissions,
da.created,
da.updated,
u.login AS user_login,
u.email AS user_email,
ug.name AS user_group
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=?`
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 = ?
-- 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
WHERE dash.id = ? AND dash.has_acl = 0 AND da.dashboard_id = -1
`
query.Result = make([]*m.DashboardAclInfoDTO, 0)
err := x.SQL(rawSQL, query.DashboardId).Find(&query.Result)
err := x.SQL(rawSQL, query.DashboardId, query.DashboardId).Find(&query.Result)
for _, p := range query.Result {
p.PermissionName = p.Permissions.String()
p.PermissionName = p.Permission.String()
}
return err
......
......@@ -20,7 +20,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
OrgId: 1,
DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_EDIT,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldEqual, m.ErrDashboardAclInfoMissing)
})
......@@ -30,7 +30,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
OrgId: 1,
UserId: currentUser.Id,
DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_EDIT,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
......@@ -49,7 +49,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
OrgId: 1,
UserId: currentUser.Id,
DashboardId: childDash.Id,
Permissions: m.PERMISSION_EDIT,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
......@@ -67,23 +67,29 @@ func TestDashboardAclDataAccess(t *testing.T) {
})
Convey("Should be able to add dashboard permission", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1,
UserId: currentUser.Id,
DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_EDIT,
})
Permission: m.PERMISSION_EDIT,
}
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil)
So(setDashAclCmd.Result.Id, ShouldEqual, 3)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
err = GetDashboardAclInfoList(q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permissions, ShouldEqual, m.PERMISSION_EDIT)
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}}
......@@ -98,8 +104,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
OrgId: 1,
UserId: 1,
DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_READ_ONLY_EDIT,
Permission: m.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
......@@ -107,7 +114,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permissions, ShouldEqual, m.PERMISSION_READ_ONLY_EDIT)
So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.Result[0].UserId, ShouldEqual, 1)
})
......@@ -115,8 +122,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
Convey("Should be able to delete an existing permission", func() {
err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{
OrgId: 1,
AclId: 1,
AclId: setDashAclCmd.Result.Id,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
......@@ -132,20 +140,35 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(err, ShouldBeNil)
Convey("Should be able to add a user permission for a user group", func() {
err := SetDashboardAcl(&m.SetDashboardAclCommand{
setDashAclCmd := m.SetDashboardAclCommand{
OrgId: 1,
UserGroupId: group1.Result.Id,
DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_EDIT,
})
Permission: m.PERMISSION_EDIT,
}
err := SetDashboardAcl(&setDashAclCmd)
So(err, ShouldBeNil)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
err = GetDashboardAclInfoList(q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permissions, ShouldEqual, m.PERMISSION_EDIT)
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}
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() {
......@@ -153,7 +176,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
OrgId: 1,
UserGroupId: group1.Result.Id,
DashboardId: savedFolder.Id,
Permissions: m.PERMISSION_READ_ONLY_EDIT,
Permission: m.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
......@@ -162,23 +185,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permissions, ShouldEqual, m.PERMISSION_READ_ONLY_EDIT)
So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
So(q3.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: 1,
})
So(err, ShouldBeNil)
q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id}
err = GetDashboardAclInfoList(q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
})
})
})
......
......@@ -384,7 +384,7 @@ func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.Permis
OrgId: 1,
UserId: userId,
DashboardId: dashId,
Permissions: permissions,
Permission: permissions,
})
So(err, ShouldBeNil)
}
......@@ -11,21 +11,42 @@ func addDashboardAclMigrations(mg *Migrator) {
{Name: "dashboard_id", Type: DB_BigInt},
{Name: "user_id", Type: DB_BigInt, Nullable: true},
{Name: "user_group_id", Type: DB_BigInt, Nullable: true},
{Name: "permissions", Type: DB_SmallInt, Default: "4"},
{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{"org_id"}},
{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))
mg.AddMigration("create dashboard acl table", NewAddTableMigration(dashboardAclV1))
//------- indexes ------------------
mg.AddMigration("add unique index dashboard_acl_org_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[0]))
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))
}
......@@ -174,10 +174,10 @@ func TestAccountDataAccess(t *testing.T) {
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 3)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: ac1.OrgId, UserId: ac3.Id, Permissions: m.PERMISSION_EDIT})
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, Permissions: m.PERMISSION_EDIT})
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() {
......
......@@ -94,7 +94,7 @@ func TestUserGroupCommandsAndQueries(t *testing.T) {
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, Permissions: m.PERMISSION_EDIT, UserGroupId: groupId})
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permission: m.PERMISSION_EDIT, UserGroupId: groupId})
err = DeleteUserGroup(&m.DeleteUserGroupCommand{Id: groupId})
So(err, ShouldBeNil)
......
......@@ -99,7 +99,7 @@ func TestUserDataAccess(t *testing.T) {
err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId})
So(err, ShouldBeNil)
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permissions: m.PERMISSION_EDIT})
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"})
......
......@@ -8,9 +8,9 @@
<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> -->
<a class="navbar-page-btn navbar-page-btn--search" ng-click="ctrl.showSearch()">
<i class="fa fa-search"></i>
</a>
<div ng-if="::!ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn">
......
......@@ -4,66 +4,38 @@ import _ from 'lodash';
const template = `
<div class="dropdown">
<metric-segment segment="ctrl.userGroupSegment"
get-options="ctrl.debouncedSearchUserGroups($query)"
on-change="ctrl.onChange()"></metric-segment>
</div>
<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 {
userGroupSegment: any;
userGroupId: number;
debouncedSearchUserGroups: any;
group: any;
userGroupPicked: any;
debouncedSearchGroups: any;
/** @ngInject */
constructor(private backendSrv, private $scope, $sce, private uiSegmentSrv) {
this.debouncedSearchUserGroups = _.debounce(this.searchUserGroups, 500, {'leading': true, 'trailing': false});
this.resetUserGroupSegment();
this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {'leading': true, 'trailing': false});
this.reset();
}
resetUserGroupSegment() {
this.userGroupId = null;
const userGroupSegment = this.uiSegmentSrv.newSegment({
value: 'Choose',
selectMode: true,
fake: true,
cssClass: 'gf-size-auto'
});
if (!this.userGroupSegment) {
this.userGroupSegment = userGroupSegment;
} else {
this.userGroupSegment.value = userGroupSegment.value;
this.userGroupSegment.html = userGroupSegment.html;
this.userGroupSegment.value = userGroupSegment.value;
}
}
userGroupIdChanged(newVal) {
if (!newVal) {
this.resetUserGroupSegment();
}
reset() {
this.group = {text: 'Choose', value: null};
}
searchUserGroups(query: string) {
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 this.uiSegmentSrv.newSegment(ug.name); });
return _.map(result.userGroups, ug => {
return {text: ug.name, value: ug};
});
}));
}
onChange() {
this.backendSrv.get('/api/user-groups/search?perpage=10&page=1&query=' + this.userGroupSegment.value)
.then(result => {
if (!result) {
return;
}
result.userGroups.forEach(ug => {
if (ug.name === this.userGroupSegment.value) {
this.userGroupId = ug.id;
}
});
});
onChange(option) {
this.userGroupPicked({$group: option.value});
}
}
......@@ -75,11 +47,11 @@ export function userGroupPicker() {
bindToController: true,
controllerAs: 'ctrl',
scope: {
userGroupId: '=',
userGroupPicked: '&',
},
link: function(scope, elem, attrs, ctrl) {
scope.$watch("ctrl.userGroupId", (newVal, oldVal) => {
ctrl.userGroupIdChanged(newVal);
scope.$on("user-group-picker-reset", () => {
ctrl.reset();
});
}
};
......
......@@ -7,37 +7,35 @@ const template = `
<gf-form-dropdown model="ctrl.user"
get-options="ctrl.debouncedSearchUsers($query)"
css-class="gf-size-auto"
on-change="ctrl.onChange()"
on-change="ctrl.onChange($option)"
</gf-form-dropdown>
</div>
`;
export class UserPickerCtrl {
user: any;
userId: number;
debouncedSearchUsers: any;
userPicked: any;
/** @ngInject */
constructor(private backendSrv, private $scope, $sce) {
this.user = {text: 'Choose', value: null};
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.id};
return {text: user.login + ' - ' + user.email, value: user};
});
}));
}
onChange() {
this.userId = this.user.value;
onChange(option) {
this.userPicked({$user: option.value});
}
userIdChanged() {
if (this.userId === null) {
this.user = {text: 'Choose', value: null};
}
reset() {
this.user = {text: 'Choose', value: null};
}
}
......@@ -56,11 +54,11 @@ export function userPicker() {
bindToController: true,
controllerAs: 'ctrl',
scope: {
userId: '=',
userPicked: '&',
},
link: function(scope, elem, attrs, ctrl) {
scope.$watch("ctrl.userId", (newVal, oldVal) => {
ctrl.userIdChanged(newVal);
scope.$on("user-picker-reset", () => {
ctrl.reset();
});
}
};
......
......@@ -2,8 +2,9 @@ define([
'jquery',
'angular',
'../core_module',
'lodash',
],
function ($, angular, coreModule) {
function ($, angular, coreModule, _) {
'use strict';
var editViewMap = {
......@@ -12,7 +13,8 @@ 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 }
};
coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) {
......@@ -20,6 +22,7 @@ function ($, angular, coreModule) {
restrict: 'A',
link: function(scope, elem) {
var editorScope;
var modalScope;
var lastEditView;
function hideEditorPane(hideToShowOtherView) {
......@@ -31,8 +34,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) {
......@@ -46,6 +48,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;
......@@ -61,19 +68,24 @@ function ($, angular, coreModule) {
var urlParams = $location.search();
if (options.editview === urlParams.editview) {
delete urlParams.editview;
$location.search(urlParams);
// hack for consistently updating url
setTimeout(function() {
$rootScope.$apply(function() {
$location.search(urlParams);
});
});
}
}
};
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'
});
......
......@@ -168,6 +168,12 @@ export class NavModelSrv {
clickHandler: () => dashNavCtrl.openEditView('annotations')
});
menu.push({
title: 'Permissions...',
icon: 'fa fa-fw fa-lock',
clickHandler: () => dashNavCtrl.openEditView('permissions')
});
if (!dashboard.meta.isHome) {
menu.push({
title: 'Version history',
......@@ -199,7 +205,7 @@ export class NavModelSrv {
if (this.contextSrv.isEditor && !dashboard.meta.isFolder) {
menu.push({
title: 'Save As ...',
title: 'Save As...',
icon: 'fa fa-fw fa-save',
clickHandler: () => dashNavCtrl.saveDashboardAs()
});
......
<div class="editor-row">
<h5 class="section-heading">Add New Permission</h5>
<form name="addPermission" class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">Type</span>
<select class="gf-form-input gf-size-auto" ng-model="ctrl.type" ng-options="r for r in ['User Group', 'User']"></select>
</div>
<div class="gf-form" ng-show="ctrl.type === 'User'">
<span class="gf-form-label">User</span>
<user-picker user-id="ctrl.userId"></user-picker>
</div>
<div class="gf-form" ng-show="ctrl.type === 'User Group'">
<span class="gf-form-label">User Group</span>
<user-group-picker user-group-id="ctrl.userGroupId"></user-group-picker>
</div>
<div class="gf-form">
<span class="gf-form-label">Permission</span>
<select class="gf-form-input gf-size-auto" ng-model="ctrl.permission" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions"></select>
</div>
<div class="gf-form">
<button class="btn gf-form-btn btn-success" ng-click="ctrl.addPermission()">Add</button>
</div>
</div>
</form>
<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="permissionlist">
<div class="permissionlist__section">
<div class="permissionlist__section-header">
<h6>Permissions</h6>
<div class="modal-content">
<table class="filter-table gf-form-group">
<tr ng-repeat="acl in ctrl.aclItems">
<td style="width: 100%;">
<i class="{{acl.icon}}"></i>
<span ng-bind-html="acl.nameHtml"></span>
</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)"></select>
</div>
</td>
<td>
<a class="btn btn-inverse btn-small" ng-click="ctrl.removeItem($index)">
<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>
<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>
<table class="filter-table form-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-repeat="permission in ctrl.userPermissions" class="permissionlist__item">
<td><i class="fa fa-fw fa-user"></i></td>
<td>{{permission.userLogin}}</td>
<td><select class="gf-form-input gf-size-auto" ng-model="permission.permissions" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions" ng-change="ctrl.updatePermission(permission)"></select></td>
<td class="text-right">
<a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
<tr ng-repeat="permission in ctrl.userGroupPermissions" class="permissionlist__item">
<td><i class="fa fa-fw fa-users"></i></td>
<td>{{permission.userGroup}}</td>
<td><select class="gf-form-input gf-size-auto" ng-model="permission.permissions" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions" ng-change="ctrl.updatePermission(permission)"></select></td>
<td class="text-right">
<a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
<tr ng-repeat="role in ctrl.roles" class="permissionlist__item">
<td></td>
<td>{{role.name}}</td>
<td><select class="gf-form-input gf-size-auto" ng-model="role.permissions" ng-options="p.value as p.text for p in ctrl.roleOptions" ng-change="ctrl.updatePermission(role)"></select></td>
<td class="text-right">
</form>
<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>
</td>
</tr>
</tbody>
</table>
</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> -->
......@@ -5,117 +5,126 @@ import appEvents from 'app/core/app_events';
import _ from 'lodash';
export class AclCtrl {
tabIndex: any;
dashboard: any;
userPermissions: Permission[];
userGroupPermissions: Permission[];
permissionTypeOptions = [
aclItems: DashboardAcl[];
permissionOptions = [
{value: 1, text: 'View'},
{value: 2, text: 'Read-only Edit'},
{value: 4, text: 'Edit'}
{value: 2, text: 'Edit'},
{value: 4, text: 'Admin'}
];
roleOptions = [
{value: 0, text: 'None'},
{value: 1, text: 'View'},
{value: 2, text: 'Read-only Edit'},
{value: 4, text: 'Edit'}
aclTypes = [
{value: 'Group', text: 'User Group'},
{value: 'User', text: 'User'},
{value: 'Viewer', text: 'Everyone With Viewer Role'},
{value: 'Editor', text: 'Everyone With Editor Role'}
];
roles = [];
type = 'User Group';
permission = 1;
userId: number;
userGroupId: number;
newType: string;
canUpdate: boolean;
/** @ngInject */
constructor(private backendSrv, private $scope) {
this.tabIndex = 0;
this.userPermissions = [];
this.userGroupPermissions = [];
constructor(private backendSrv, private dashboardSrv, private $sce, private $scope) {
this.aclItems = [];
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.userPermissions = _.filter(result, p => { return p.userId > 0;});
this.userGroupPermissions = _.filter(result, p => { return p.userGroupId > 0;});
this.roles = this.setRoles(result);
this.aclItems = _.map(result, this.prepareViewModel.bind(this));
});
}
setRoles(result: any) {
return [
{name: 'Org Viewer', permissions: 1},
{name: 'Org Read Only Editor', permissions: 2},
{name: 'Org Editor', permissions: 4},
{name: 'Org Admin', permissions: 4}
];
}
addPermission() {
if (this.type === 'User') {
if (!this.userId) {
return;
}
return this.addOrUpdateUserPermission(this.userId, this.permission).then(() => {
this.userId = null;
return this.get(this.dashboard.id);
});
} else {
if (!this.userGroupId) {
return;
}
return this.addOrUpdateUserGroupPermission(this.userGroupId, this.permission).then(() => {
this.userGroupId = null;
return this.get(this.dashboard.id);
});
prepareViewModel(item: DashboardAcl): DashboardAcl {
if (item.userId > 0) {
item.icon = "fa fa-fw fa-user";
item.nameHtml = this.$sce.trustAsHtml(item.userLogin);
} else if (item.userGroupId > 0) {
item.icon = "fa fa-fw fa-users";
item.nameHtml = this.$sce.trustAsHtml(item.userGroup);
} 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`);
}
}
addOrUpdateUserPermission(userId: number, permissionType: number) {
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, {
userId: userId,
permissions: permissionType
});
return item;
}
addOrUpdateUserGroupPermission(userGroupId: number, permissionType: number) {
update() {
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, {
userGroupId: userGroupId,
permissions: permissionType
acl: this.aclItems.map(item => {
return {
id: item.id,
userId: item.userId,
userGroupId: item.userGroupId,
role: item.role,
permission: item.permission,
};
})
});
}
updatePermission(permission: any) {
if (permission.userId > 0) {
return this.addOrUpdateUserPermission(permission.userId, permission.permissions);
} else {
if (!permission.userGroupId) {
return;
}
return this.addOrUpdateUserGroupPermission(permission.userGroupId, permission.permissions);
typeChanged() {
if (this.newType === 'Viewer' || this.newType === 'Editor') {
this.aclItems.push(this.prepareViewModel({
permission: 1,
role: this.newType
}));
this.canUpdate = true;
this.resetNewType();
}
}
removePermission(permission: Permission) {
return this.backendSrv.delete(`/api/dashboards/id/${permission.dashboardId}/acl/${permission.id}`).then(() => {
return this.get(permission.dashboardId);
});
permissionChanged() {
this.canUpdate = true;
}
userPicked(user) {
this.aclItems.push(this.prepareViewModel({
userId: user.id,
userLogin: user.login,
permission: 1,
}));
this.canUpdate = true;
this.$scope.$broadcast('user-picker-reset');
}
groupPicked(group) {
console.log(group);
this.aclItems.push(this.prepareViewModel({
userGroupId: group.id,
userGroup: group.name,
permission: 1,
}));
this.canUpdate = true;
this.$scope.$broadcast('user-group-picker-reset');
}
removeItem(index) {
this.aclItems.splice(index, 1);
this.canUpdate = true;
}
}
export function aclSettings() {
export function dashAclModal() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/acl/acl.html',
controller: AclCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: { dashboard: "=" }
scope: {
dismiss: "&"
}
};
}
......@@ -126,19 +135,19 @@ export interface FormModel {
PermissionType: number;
}
export interface Permission {
id: number;
orgId: number;
dashboardId: number;
created: Date;
updated: Date;
userId: number;
userLogin: number;
userEmail: string;
userGroupId: number;
userGroup: string;
permissions: string[];
permissionType: number[];
export interface DashboardAcl {
id?: number;
dashboardId?: number;
userId?: number;
userLogin?: number;
userEmail?: string;
userGroupId?: number;
userGroup?: string;
permission?: number;
permissionName?: string;
role?: string;
icon?: string;
nameHtml?: string;
}
coreModule.directive('aclSettings', aclSettings);
coreModule.directive('dashAclModal', dashAclModal);
<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>
</a>
<navbar model="ctrl.navModel">
<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>
<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 dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
<li>
<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
</li>
<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 ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
<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 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 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 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>
<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>
<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>
</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>
</div>
</div>
<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>
......@@ -143,17 +143,6 @@ export class DashNavCtrl {
onFolderChange(parentId) {
this.dashboard.parentId = parentId;
}
showSearch() {
this.$rootScope.appEvent('show-dash-search');
}
navItemClicked(navItem, evt) {
if (navItem.clickHandler) {
navItem.clickHandler();
evt.preventDefault();
}
}
}
export function dashNavDirective() {
......
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
......
......@@ -275,3 +275,22 @@
content: "\f11c";
}
}
.dropdown-menu.dropdown-menu--new {
li a {
padding: $spacer/2 $spacer;
border-left: 2px solid $side-menu-bg;
background: $side-menu-bg;
i {
display: inline-block;
padding-right: 21px;
}
&:hover {
@include left-brand-border-gradient();
color: $link-hover-color;
background: $input-label-bg;
}
}
}
......@@ -149,7 +149,6 @@
}
.dropdown-menu.dropdown-menu--navbar {
top: 100%;
min-width: 100%;
margin-top: 0px;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment