diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba7b4a1aff..f6512314d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### WIP (in develop branch currently as its unstable or unfinished) - Dashboard folders -- User groups +- Teams - Dashboard permissions (on folder & dashboard level), permissions can be assigned to groups or individual users - UX changes to nav & side menu - New dashboard grid layout system diff --git a/ROADMAP.md b/ROADMAP.md index 4273d8df6a9..479c1933bc0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,7 +6,7 @@ But it will give you an idea of our current vision and plan. ### Short term (1-4 months) - Release Grafana v5 - - User groups + - Teams - Dashboard folders - Dashboard & folder permissions (assigned to users or groups) - New Dashboard layout engine diff --git a/pkg/api/api.go b/pkg/api/api.go index b707dc17e21..a37f200f9d1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -135,16 +135,16 @@ func (hs *HttpServer) registerRoutes() { usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg)) }, reqGrafanaAdmin) - // user group (admin permission required) - apiRoute.Group("/user-groups", func(userGroupsRoute RouteRegister) { - userGroupsRoute.Get("/:userGroupId", wrap(GetUserGroupById)) - userGroupsRoute.Get("/search", wrap(SearchUserGroups)) - userGroupsRoute.Post("/", quota("user-groups"), bind(m.CreateUserGroupCommand{}), wrap(CreateUserGroup)) - userGroupsRoute.Put("/:userGroupId", bind(m.UpdateUserGroupCommand{}), wrap(UpdateUserGroup)) - userGroupsRoute.Delete("/:userGroupId", wrap(DeleteUserGroupById)) - userGroupsRoute.Get("/:userGroupId/members", wrap(GetUserGroupMembers)) - userGroupsRoute.Post("/:userGroupId/members", quota("user-groups"), bind(m.AddUserGroupMemberCommand{}), wrap(AddUserGroupMember)) - userGroupsRoute.Delete("/:userGroupId/members/:userId", wrap(RemoveUserGroupMember)) + // team (admin permission required) + apiRoute.Group("/teams", func(teamsRoute RouteRegister) { + teamsRoute.Get("/:teamId", wrap(GetTeamById)) + teamsRoute.Get("/search", wrap(SearchTeams)) + teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam)) + teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) + teamsRoute.Delete("/:teamId", wrap(DeleteTeamById)) + teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers)) + teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) + teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember)) }, reqOrgAdmin) // org information available to all users. diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go index c6847963d34..88cc74b9d1c 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_acl.go @@ -43,7 +43,7 @@ func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCom OrgId: c.OrgId, DashboardId: dashId, UserId: item.UserId, - UserGroupId: item.UserGroupId, + TeamId: item.TeamId, Role: item.Role, Permission: item.Permission, Created: time.Now(), diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_acl_test.go index 162bac0aba8..e22e625dcf9 100644 --- a/pkg/api/dashboard_acl_test.go +++ b/pkg/api/dashboard_acl_test.go @@ -16,8 +16,8 @@ func TestDashboardAclApiEndpoint(t *testing.T) { {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}, + {Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, + {Id: 5, OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, } dtoRes := transformDashboardAclsToDTOs(mockResult) @@ -31,9 +31,9 @@ func TestDashboardAclApiEndpoint(t *testing.T) { return nil }) - userGroupResp := []*m.UserGroup{} - bus.AddHandler("test", func(query *m.GetUserGroupsByUserQuery) error { - query.Result = userGroupResp + teamResp := []*m.Team{} + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { + query.Result = teamResp return nil }) @@ -81,9 +81,9 @@ func TestDashboardAclApiEndpoint(t *testing.T) { }) }) - Convey("When user is a member of a user group in the ACL with admin permission", func() { + Convey("When user is a member of a team 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"}) + teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"}) bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { return nil @@ -165,7 +165,7 @@ func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardA DashboardId: acl.DashboardId, Permission: acl.Permission, UserId: acl.UserId, - UserGroupId: acl.UserGroupId, + TeamId: acl.TeamId, } dtos = append(dtos, dto) } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 84ac01bce9a..1ddb32b4adc 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -56,8 +56,8 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *m.GetUserGroupsByUserQuery) error { - query.Result = []*m.UserGroup{} + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { + query.Result = []*m.Team{} return nil }) @@ -217,8 +217,8 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *m.GetUserGroupsByUserQuery) error { - query.Result = []*m.UserGroup{} + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { + query.Result = []*m.Team{} return nil }) diff --git a/pkg/api/dtos/acl.go b/pkg/api/dtos/acl.go index 41f560e794b..6c74e68ce0d 100644 --- a/pkg/api/dtos/acl.go +++ b/pkg/api/dtos/acl.go @@ -9,8 +9,8 @@ type UpdateDashboardAclCommand struct { } type DashboardAclUpdateItem struct { - UserId int64 `json:"userId"` - UserGroupId int64 `json:"userGroupId"` - Role *m.RoleType `json:"role,omitempty"` - Permission m.PermissionType `json:"permission"` + UserId int64 `json:"userId"` + TeamId int64 `json:"teamId"` + Role *m.RoleType `json:"role,omitempty"` + Permission m.PermissionType `json:"permission"` } diff --git a/pkg/api/index.go b/pkg/api/index.go index e179828d2f7..862e9af0519 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -231,8 +231,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Text: "Teams", Id: "teams", Description: "Manage org groups", - Icon: "gicon gicon-user-group", - Url: setting.AppSubUrl + "/org/user-groups", + Icon: "gicon gicon-team", + Url: setting.AppSubUrl + "/org/teams", }, { Text: "Plugins", diff --git a/pkg/api/team.go b/pkg/api/team.go new file mode 100644 index 00000000000..31e465d3232 --- /dev/null +++ b/pkg/api/team.go @@ -0,0 +1,92 @@ +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" +) + +// POST /api/teams +func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response { + cmd.OrgId = c.OrgId + if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrTeamNameTaken { + return ApiError(409, "Team name taken", err) + } + return ApiError(500, "Failed to create Team", err) + } + + return Json(200, &util.DynMap{ + "teamId": cmd.Result.Id, + "message": "Team created", + }) +} + +// PUT /api/teams/:teamId +func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response { + cmd.Id = c.ParamsInt64(":teamId") + if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrTeamNameTaken { + return ApiError(400, "Team name taken", err) + } + return ApiError(500, "Failed to update Team", err) + } + + return ApiSuccess("Team updated") +} + +// DELETE /api/teams/:teamId +func DeleteTeamById(c *middleware.Context) Response { + if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil { + if err == m.ErrTeamNotFound { + return ApiError(404, "Failed to delete Team. ID not found", nil) + } + return ApiError(500, "Failed to update Team", err) + } + return ApiSuccess("Team deleted") +} + +// GET /api/teams/search +func SearchTeams(c *middleware.Context) Response { + perPage := c.QueryInt("perpage") + if perPage <= 0 { + perPage = 1000 + } + page := c.QueryInt("page") + if page < 1 { + page = 1 + } + + query := m.SearchTeamsQuery{ + 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 Teams", err) + } + + query.Result.Page = page + query.Result.PerPage = perPage + + return Json(200, query.Result) +} + +// GET /api/teams/:teamId +func GetTeamById(c *middleware.Context) Response { + query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")} + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrTeamNotFound { + return ApiError(404, "Team not found", err) + } + + return ApiError(500, "Failed to get Team", err) + } + + return Json(200, &query.Result) +} diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go new file mode 100644 index 00000000000..0999c9573a5 --- /dev/null +++ b/pkg/api/team_members.go @@ -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/teams/:teamId/members +func GetTeamMembers(c *middleware.Context) Response { + query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")} + + if err := bus.Dispatch(&query); err != nil { + return ApiError(500, "Failed to get Team Members", err) + } + + return Json(200, query.Result) +} + +// POST /api/teams/:teamId/members +func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response { + cmd.TeamId = c.ParamsInt64(":teamId") + cmd.OrgId = c.OrgId + + if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrTeamMemberAlreadyAdded { + return ApiError(400, "User is already added to this team", err) + } + return ApiError(500, "Failed to add Member to Team", err) + } + + return Json(200, &util.DynMap{ + "message": "Member added to Team", + }) +} + +// DELETE /api/teams/:teamId/members/:userId +func RemoveTeamMember(c *middleware.Context) Response { + if err := bus.Dispatch(&m.RemoveTeamMemberCommand{TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil { + return ApiError(500, "Failed to remove Member from Team", err) + } + return ApiSuccess("Team Member removed") +} diff --git a/pkg/api/user_group_test.go b/pkg/api/team_test.go similarity index 61% rename from pkg/api/user_group_test.go rename to pkg/api/team_test.go index 730461ac8e8..535d24ff553 100644 --- a/pkg/api/user_group_test.go +++ b/pkg/api/team_test.go @@ -10,21 +10,21 @@ import ( . "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"}, +func TestTeamApiEndpoint(t *testing.T) { + Convey("Given two teams", t, func() { + mockResult := models.SearchTeamQueryResult{ + Teams: []*models.Team{ + {Name: "team1"}, + {Name: "team2"}, }, TotalCount: 2, } Convey("When searching with no parameters", func() { - loggedInUserScenario("When calling GET on", "/api/user-groups/search", func(sc *scenarioContext) { + loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) { var sentLimit int var sendPage int - bus.AddHandler("test", func(query *models.SearchUserGroupsQuery) error { + bus.AddHandler("test", func(query *models.SearchTeamsQuery) error { query.Result = mockResult sentLimit = query.Limit @@ -33,7 +33,7 @@ func TestUserGroupApiEndpoint(t *testing.T) { return nil }) - sc.handlerFunc = SearchUserGroups + sc.handlerFunc = SearchTeams sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() So(sentLimit, ShouldEqual, 1000) @@ -43,15 +43,15 @@ func TestUserGroupApiEndpoint(t *testing.T) { So(err, ShouldBeNil) So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2) - So(len(respJSON.Get("userGroups").MustArray()), ShouldEqual, 2) + So(len(respJSON.Get("teams").MustArray()), ShouldEqual, 2) }) }) Convey("When searching with page and perpage parameters", func() { - loggedInUserScenario("When calling GET on", "/api/user-groups/search", func(sc *scenarioContext) { + loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) { var sentLimit int var sendPage int - bus.AddHandler("test", func(query *models.SearchUserGroupsQuery) error { + bus.AddHandler("test", func(query *models.SearchTeamsQuery) error { query.Result = mockResult sentLimit = query.Limit @@ -60,7 +60,7 @@ func TestUserGroupApiEndpoint(t *testing.T) { return nil }) - sc.handlerFunc = SearchUserGroups + sc.handlerFunc = SearchTeams sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() So(sentLimit, ShouldEqual, 10) diff --git a/pkg/api/user_group.go b/pkg/api/user_group.go deleted file mode 100644 index 8371881aecb..00000000000 --- a/pkg/api/user_group.go +++ /dev/null @@ -1,92 +0,0 @@ -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" -) - -// 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) - } - - 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) -} diff --git a/pkg/api/user_group_members.go b/pkg/api/user_group_members.go deleted file mode 100644 index 09f3def59bd..00000000000 --- a/pkg/api/user_group_members.go +++ /dev/null @@ -1,44 +0,0 @@ -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") -} diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 9b65ec8eb33..fa7ad00de7f 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -24,7 +24,7 @@ func (p PermissionType) String() string { // Typed errors var ( - ErrDashboardAclInfoMissing = errors.New("User id and user group id cannot both be empty for a dashboard permission.") + ErrDashboardAclInfoMissing = errors.New("User id and team id cannot both be empty for a dashboard permission.") ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.") ) @@ -34,10 +34,10 @@ type DashboardAcl struct { OrgId int64 DashboardId int64 - UserId int64 - UserGroupId int64 - Role *RoleType // pointer to be nullable - Permission PermissionType + UserId int64 + TeamId int64 + Role *RoleType // pointer to be nullable + Permission PermissionType Created time.Time Updated time.Time @@ -54,8 +54,8 @@ type DashboardAclInfoDTO struct { UserId int64 `json:"userId"` UserLogin string `json:"userLogin"` UserEmail string `json:"userEmail"` - UserGroupId int64 `json:"userGroupId"` - UserGroup string `json:"userGroup"` + TeamId int64 `json:"teamId"` + Team string `json:"team"` Role *RoleType `json:"role,omitempty"` Permission PermissionType `json:"permission"` PermissionName string `json:"permissionName"` @@ -74,7 +74,7 @@ type SetDashboardAclCommand struct { DashboardId int64 OrgId int64 UserId int64 - UserGroupId int64 + TeamId int64 Permission PermissionType Result DashboardAcl diff --git a/pkg/models/team.go b/pkg/models/team.go new file mode 100644 index 00000000000..a8f53e81a1e --- /dev/null +++ b/pkg/models/team.go @@ -0,0 +1,68 @@ +package models + +import ( + "errors" + "time" +) + +// Typed errors +var ( + ErrTeamNotFound = errors.New("Team not found") + ErrTeamNameTaken = errors.New("Team name is taken") +) + +// Team model +type Team 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 CreateTeamCommand struct { + Name string `json:"name" binding:"Required"` + OrgId int64 `json:"-"` + + Result Team `json:"-"` +} + +type UpdateTeamCommand struct { + Id int64 + Name string +} + +type DeleteTeamCommand struct { + Id int64 +} + +type GetTeamByIdQuery struct { + Id int64 + Result *Team +} + +type GetTeamsByUserQuery struct { + UserId int64 `json:"userId"` + Result []*Team `json:"teams"` +} + +type SearchTeamsQuery struct { + Query string + Name string + Limit int + Page int + OrgId int64 + + Result SearchTeamQueryResult +} + +type SearchTeamQueryResult struct { + TotalCount int64 `json:"totalCount"` + Teams []*Team `json:"teams"` + Page int `json:"page"` + PerPage int `json:"perPage"` +} diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go new file mode 100644 index 00000000000..71e5cd4ba12 --- /dev/null +++ b/pkg/models/team_member.go @@ -0,0 +1,55 @@ +package models + +import ( + "errors" + "time" +) + +// Typed errors +var ( + ErrTeamMemberAlreadyAdded = errors.New("User is already added to this team") +) + +// TeamMember model +type TeamMember struct { + Id int64 + OrgId int64 + TeamId int64 + UserId int64 + + Created time.Time + Updated time.Time +} + +// --------------------- +// COMMANDS + +type AddTeamMemberCommand struct { + UserId int64 `json:"userId" binding:"Required"` + OrgId int64 `json:"-"` + TeamId int64 `json:"-"` +} + +type RemoveTeamMemberCommand struct { + UserId int64 + TeamId int64 +} + +// ---------------------- +// QUERIES + +type GetTeamMembersQuery struct { + TeamId int64 + Result []*TeamMemberDTO +} + +// ---------------------- +// Projections and DTOs + +type TeamMemberDTO struct { + OrgId int64 `json:"orgId"` + TeamId int64 `json:"teamId"` + UserId int64 `json:"userId"` + Email string `json:"email"` + Login string `json:"login"` +} diff --git a/pkg/models/user_group.go b/pkg/models/user_group.go deleted file mode 100644 index e5e361526d4..00000000000 --- a/pkg/models/user_group.go +++ /dev/null @@ -1,68 +0,0 @@ -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"` -} diff --git a/pkg/models/user_group_member.go b/pkg/models/user_group_member.go deleted file mode 100644 index 23ddd84601b..00000000000 --- a/pkg/models/user_group_member.go +++ /dev/null @@ -1,55 +0,0 @@ -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"` -} diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index a05213e87c8..72fe00a4ceb 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -11,7 +11,7 @@ type DashboardGuardian struct { dashId int64 orgId int64 acl []*m.DashboardAclInfoDTO - groups []*m.UserGroup + groups []*m.Team log log.Logger } @@ -55,7 +55,7 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, er orgRole = m.ROLE_VIEWER } - userGroupAclItems := []*m.DashboardAclInfoDTO{} + teamAclItems := []*m.DashboardAclInfoDTO{} for _, p := range acl { // user match @@ -71,26 +71,26 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, er } // remember this rule for later - if p.UserGroupId > 0 { - userGroupAclItems = append(userGroupAclItems, p) + if p.TeamId > 0 { + teamAclItems = append(teamAclItems, p) } } // do we have group rules? - if len(userGroupAclItems) == 0 { + if len(teamAclItems) == 0 { return false, nil } // load groups - userGroups, err := g.getUserGroups() + teams, err := g.getTeams() 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 { + for _, ug := range teams { + if ug.Id == p.TeamId && p.Permission >= permission { return true, nil } } @@ -114,12 +114,12 @@ func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) { return g.acl, nil } -func (g *DashboardGuardian) getUserGroups() ([]*m.UserGroup, error) { +func (g *DashboardGuardian) getTeams() ([]*m.Team, error) { if g.groups != nil { return g.groups, nil } - query := m.GetUserGroupsByUserQuery{UserId: g.user.UserId} + query := m.GetTeamsByUserQuery{UserId: g.user.UserId} err := bus.Dispatch(&query) g.groups = query.Result diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 8d3a2455f1f..3ab0361d175 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -24,7 +24,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { } for _, item := range cmd.Items { - if item.UserId == 0 && item.UserGroupId == 0 && !item.Role.IsValid() { + if item.UserId == 0 && item.TeamId == 0 && !item.Role.IsValid() { return m.ErrDashboardAclInfoMissing } @@ -32,7 +32,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { return m.ErrDashboardPermissionDashboardEmpty } - sess.Nullable("user_id", "user_group_id") + sess.Nullable("user_id", "team_id") if _, err := sess.Insert(item); err != nil { return err } @@ -49,7 +49,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { return inTransaction(func(sess *DBSession) error { - if cmd.UserId == 0 && cmd.UserGroupId == 0 { + if cmd.UserId == 0 && cmd.TeamId == 0 { return m.ErrDashboardAclInfoMissing } @@ -57,7 +57,7 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { 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 { + if res, err := sess.Query("SELECT 1 from "+dialect.Quote("dashboard_acl")+" WHERE dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId); err != nil { return err } else if len(res) == 1 { @@ -66,7 +66,7 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { 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 { + if _, err := sess.Cols("updated", "permission").Where("dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId).Update(&entity); err != nil { return err } @@ -75,7 +75,7 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { entity := m.DashboardAcl{ OrgId: cmd.OrgId, - UserGroupId: cmd.UserGroupId, + TeamId: cmd.TeamId, UserId: cmd.UserId, Created: time.Now(), Updated: time.Now(), @@ -89,8 +89,8 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { cols = append(cols, "user_id") } - if cmd.UserGroupId != 0 { - cols = append(cols, "user_group_id") + if cmd.TeamId != 0 { + cols = append(cols, "team_id") } _, err := sess.Cols(cols...).Insert(&entity) @@ -138,17 +138,17 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { da.org_id, da.dashboard_id, da.user_id, - da.user_group_id, + da.team_id, da.permission, da.role, da.created, da.updated, u.login AS user_login, u.email AS user_email, - ug.name AS user_group + ug.name AS team 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 + LEFT OUTER JOIN team ug on ug.id = da.team_id WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ? -- Also include default permission if has_acl = 0 @@ -159,14 +159,14 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { da.org_id, da.dashboard_id, da.user_id, - da.user_group_id, + da.team_id, da.permission, da.role, da.created, da.updated, '' as user_login, '' as user_email, - '' as user_group + '' as team FROM dashboard_acl as da, dashboard as dash LEFT JOIN dashboard folder on dash.folder_id = folder.id diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index 55f4b24808b..bb6363883d6 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -16,7 +16,7 @@ func TestDashboardAclDataAccess(t *testing.T) { 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() { + Convey("When adding dashboard permission with userId and teamId set to 0", func() { err := SetDashboardAcl(&m.SetDashboardAclCommand{ OrgId: 1, DashboardId: savedFolder.Id, @@ -175,15 +175,15 @@ func TestDashboardAclDataAccess(t *testing.T) { }) }) - Convey("Given a user group", func() { - group1 := m.CreateUserGroupCommand{Name: "group1 name", OrgId: 1} - err := CreateUserGroup(&group1) + Convey("Given a team", func() { + group1 := m.CreateTeamCommand{Name: "group1 name", OrgId: 1} + err := CreateTeam(&group1) So(err, ShouldBeNil) - Convey("Should be able to add a user permission for a user group", func() { + Convey("Should be able to add a user permission for a team", func() { setDashAclCmd := m.SetDashboardAclCommand{ OrgId: 1, - UserGroupId: group1.Result.Id, + TeamId: group1.Result.Id, DashboardId: savedFolder.Id, Permission: m.PERMISSION_EDIT, } @@ -196,9 +196,9 @@ func TestDashboardAclDataAccess(t *testing.T) { 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) + So(q1.Result[0].TeamId, ShouldEqual, group1.Result.Id) - Convey("Should be able to delete an existing permission for a user group", func() { + Convey("Should be able to delete an existing permission for a team", func() { err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{ OrgId: 1, AclId: setDashAclCmd.Result.Id, @@ -212,10 +212,10 @@ func TestDashboardAclDataAccess(t *testing.T) { }) }) - Convey("Should be able to update an existing permission for a user group", func() { + Convey("Should be able to update an existing permission for a team", func() { err := SetDashboardAcl(&m.SetDashboardAclCommand{ OrgId: 1, - UserGroupId: group1.Result.Id, + TeamId: group1.Result.Id, DashboardId: savedFolder.Id, Permission: m.PERMISSION_ADMIN, }) @@ -227,7 +227,7 @@ func TestDashboardAclDataAccess(t *testing.T) { 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) + So(q3.Result[0].TeamId, ShouldEqual, group1.Result.Id) }) }) diff --git a/pkg/services/sqlstore/migrations/dashboard_acl.go b/pkg/services/sqlstore/migrations/dashboard_acl.go index af85e48af0d..5d92f721d38 100644 --- a/pkg/services/sqlstore/migrations/dashboard_acl.go +++ b/pkg/services/sqlstore/migrations/dashboard_acl.go @@ -10,7 +10,7 @@ func addDashboardAclMigrations(mg *Migrator) { {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: "team_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}, @@ -19,7 +19,7 @@ func addDashboardAclMigrations(mg *Migrator) { Indices: []*Index{ {Cols: []string{"dashboard_id"}}, {Cols: []string{"dashboard_id", "user_id"}, Type: UniqueIndex}, - {Cols: []string{"dashboard_id", "user_group_id"}, Type: UniqueIndex}, + {Cols: []string{"dashboard_id", "team_id"}, Type: UniqueIndex}, }, } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 3cd4f4399fb..8e9268779ef 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -26,7 +26,7 @@ func AddMigrations(mg *Migrator) { addAnnotationMig(mg) addTestDataMigrations(mg) addDashboardVersionMigration(mg) - addUserGroupMigrations(mg) + addTeamMigrations(mg) addDashboardAclMigrations(mg) addTagMigration(mg) } diff --git a/pkg/services/sqlstore/migrations/user_group_mig.go b/pkg/services/sqlstore/migrations/team_mig.go similarity index 50% rename from pkg/services/sqlstore/migrations/user_group_mig.go rename to pkg/services/sqlstore/migrations/team_mig.go index 589def0c504..cc479097f9b 100644 --- a/pkg/services/sqlstore/migrations/user_group_mig.go +++ b/pkg/services/sqlstore/migrations/team_mig.go @@ -2,9 +2,9 @@ package migrations import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" -func addUserGroupMigrations(mg *Migrator) { - userGroupV1 := Table{ - Name: "user_group", +func addTeamMigrations(mg *Migrator) { + teamV1 := Table{ + Name: "team", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, @@ -18,31 +18,31 @@ func addUserGroupMigrations(mg *Migrator) { }, } - mg.AddMigration("create user group table", NewAddTableMigration(userGroupV1)) + mg.AddMigration("create team table", NewAddTableMigration(teamV1)) //------- 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])) + mg.AddMigration("add index team.org_id", NewAddIndexMigration(teamV1, teamV1.Indices[0])) + mg.AddMigration("add unique index team_org_id_name", NewAddIndexMigration(teamV1, teamV1.Indices[1])) - userGroupMemberV1 := Table{ - Name: "user_group_member", + teamMemberV1 := Table{ + Name: "team_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: "team_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}, + {Cols: []string{"org_id", "team_id", "user_id"}, Type: UniqueIndex}, }, } - mg.AddMigration("create user group member table", NewAddTableMigration(userGroupMemberV1)) + mg.AddMigration("create team member table", NewAddTableMigration(teamMemberV1)) //------- 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])) + mg.AddMigration("add index team_member.org_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[0])) + mg.AddMigration("add unique index team_member_org_id_team_id_user_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[1])) } diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 2acb623bd34..2c2a51fd362 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -91,7 +91,7 @@ func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error { 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 = ?", + "DELETE FROM team_member WHERE org_id=? and user_id = ?", } for _, sql := range deletes { diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index 99bde21d171..6a5e8e60b54 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -179,7 +179,7 @@ func (sb *SearchBuilder) buildSearchWhereClause() { 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 team_member as ugm on ugm.team_id = da.team_id LEFT JOIN org_user ou on ou.role = da.role WHERE d.has_acl = 1 and diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go new file mode 100644 index 00000000000..2bc04bad0c6 --- /dev/null +++ b/pkg/services/sqlstore/team.go @@ -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", CreateTeam) + bus.AddHandler("sql", UpdateTeam) + bus.AddHandler("sql", DeleteTeam) + bus.AddHandler("sql", SearchTeams) + bus.AddHandler("sql", GetTeamById) + bus.AddHandler("sql", GetTeamsByUser) + + bus.AddHandler("sql", AddTeamMember) + bus.AddHandler("sql", RemoveTeamMember) + bus.AddHandler("sql", GetTeamMembers) +} + +func CreateTeam(cmd *m.CreateTeamCommand) error { + return inTransaction(func(sess *DBSession) error { + + if isNameTaken, err := isTeamNameTaken(cmd.Name, 0, sess); err != nil { + return err + } else if isNameTaken { + return m.ErrTeamNameTaken + } + + team := m.Team{ + Name: cmd.Name, + OrgId: cmd.OrgId, + Created: time.Now(), + Updated: time.Now(), + } + + _, err := sess.Insert(&team) + + cmd.Result = team + + return err + }) +} + +func UpdateTeam(cmd *m.UpdateTeamCommand) error { + return inTransaction(func(sess *DBSession) error { + + if isNameTaken, err := isTeamNameTaken(cmd.Name, cmd.Id, sess); err != nil { + return err + } else if isNameTaken { + return m.ErrTeamNameTaken + } + + team := m.Team{ + Name: cmd.Name, + Updated: time.Now(), + } + + affectedRows, err := sess.Id(cmd.Id).Update(&team) + + if err != nil { + return err + } + + if affectedRows == 0 { + return m.ErrTeamNotFound + } + + return nil + }) +} + +func DeleteTeam(cmd *m.DeleteTeamCommand) error { + return inTransaction(func(sess *DBSession) error { + if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.Id); err != nil { + return err + } else if len(res) != 1 { + return m.ErrTeamNotFound + } + + deletes := []string{ + "DELETE FROM team_member WHERE team_id = ?", + "DELETE FROM team WHERE id = ?", + "DELETE FROM dashboard_acl WHERE team_id = ?", + } + + for _, sql := range deletes { + _, err := sess.Exec(sql, cmd.Id) + if err != nil { + return err + } + } + return nil + }) +} + +func isTeamNameTaken(name string, existingId int64, sess *DBSession) (bool, error) { + var team m.Team + exists, err := sess.Where("name=?", name).Get(&team) + + if err != nil { + return false, nil + } + + if exists && existingId != team.Id { + return true, nil + } + + return false, nil +} + +func SearchTeams(query *m.SearchTeamsQuery) error { + query.Result = m.SearchTeamQueryResult{ + Teams: make([]*m.Team, 0), + } + queryWithWildcards := "%" + query.Query + "%" + + sess := x.Table("team") + 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.Teams); err != nil { + return err + } + + team := m.Team{} + + countSess := x.Table("team") + if query.Query != "" { + countSess.Where("name LIKE ?", queryWithWildcards) + } + if query.Name != "" { + countSess.Where("name=?", query.Name) + } + count, err := countSess.Count(&team) + query.Result.TotalCount = count + + return err +} + +func GetTeamById(query *m.GetTeamByIdQuery) error { + var team m.Team + exists, err := x.Id(query.Id).Get(&team) + if err != nil { + return err + } + + if !exists { + return m.ErrTeamNotFound + } + + query.Result = &team + return nil +} + +func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { + query.Result = make([]*m.Team, 0) + + sess := x.Table("team") + sess.Join("INNER", "team_member", "team.id=team_member.team_id") + sess.Where("team_member.user_id=?", query.UserId) + + err := sess.Find(&query.Result) + if err != nil { + return err + } + + return nil +} + +func AddTeamMember(cmd *m.AddTeamMemberCommand) error { + return inTransaction(func(sess *DBSession) error { + if res, err := sess.Query("SELECT 1 from team_member WHERE team_id=? and user_id=?", cmd.TeamId, cmd.UserId); err != nil { + return err + } else if len(res) == 1 { + return m.ErrTeamMemberAlreadyAdded + } + + if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.TeamId); err != nil { + return err + } else if len(res) != 1 { + return m.ErrTeamNotFound + } + + entity := m.TeamMember{ + OrgId: cmd.OrgId, + TeamId: cmd.TeamId, + UserId: cmd.UserId, + Created: time.Now(), + Updated: time.Now(), + } + + _, err := sess.Insert(&entity) + return err + }) +} + +func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error { + return inTransaction(func(sess *DBSession) error { + var rawSql = "DELETE FROM team_member WHERE team_id=? and user_id=?" + _, err := sess.Exec(rawSql, cmd.TeamId, cmd.UserId) + if err != nil { + return err + } + + return err + }) +} + +func GetTeamMembers(query *m.GetTeamMembersQuery) error { + query.Result = make([]*m.TeamMemberDTO, 0) + sess := x.Table("team_member") + sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user"))) + sess.Where("team_member.team_id=?", query.TeamId) + sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login") + sess.Asc("user.login", "user.email") + + err := sess.Find(&query.Result) + return err +} diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go new file mode 100644 index 00000000000..4a099db14ff --- /dev/null +++ b/pkg/services/sqlstore/team_test.go @@ -0,0 +1,114 @@ +package sqlstore + +import ( + "fmt" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" +) + +func TestTeamCommandsAndQueries(t *testing.T) { + + Convey("Testing Team commands & queries", t, func() { + InitTestDB(t) + + Convey("Given saved users and two teams", 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.CreateTeamCommand{Name: "group1 name"} + group2 := m.CreateTeamCommand{Name: "group2 name"} + + err := CreateTeam(&group1) + So(err, ShouldBeNil) + err = CreateTeam(&group2) + So(err, ShouldBeNil) + + Convey("Should be able to create teams and add users", func() { + query := &m.SearchTeamsQuery{Name: "group1 name", Page: 1, Limit: 10} + err = SearchTeams(query) + So(err, ShouldBeNil) + So(query.Page, ShouldEqual, 1) + + team1 := query.Result.Teams[0] + So(team1.Name, ShouldEqual, "group1 name") + + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: team1.Id, UserId: userIds[0]}) + So(err, ShouldBeNil) + + q1 := &m.GetTeamMembersQuery{TeamId: team1.Id} + err = GetTeamMembers(q1) + So(err, ShouldBeNil) + So(q1.Result[0].TeamId, ShouldEqual, team1.Id) + So(q1.Result[0].Login, ShouldEqual, "loginuser0") + }) + + Convey("Should be able to search for teams", func() { + query := &m.SearchTeamsQuery{Query: "group", Page: 1} + err = SearchTeams(query) + So(err, ShouldBeNil) + So(len(query.Result.Teams), ShouldEqual, 2) + So(query.Result.TotalCount, ShouldEqual, 2) + + query2 := &m.SearchTeamsQuery{Query: ""} + err = SearchTeams(query2) + So(err, ShouldBeNil) + So(len(query2.Result.Teams), ShouldEqual, 2) + }) + + Convey("Should be able to return all teams a user is member of", func() { + groupId := group2.Result.Id + err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[0]}) + + query := &m.GetTeamsByUserQuery{UserId: userIds[0]} + err = GetTeamsByUser(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 = RemoveTeamMember(&m.RemoveTeamMemberCommand{TeamId: group1.Result.Id, UserId: userIds[0]}) + So(err, ShouldBeNil) + + q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id} + err = GetTeamMembers(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 := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[1]}) + So(err, ShouldBeNil) + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[2]}) + So(err, ShouldBeNil) + err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permission: m.PERMISSION_EDIT, TeamId: groupId}) + + err = DeleteTeam(&m.DeleteTeamCommand{Id: groupId}) + So(err, ShouldBeNil) + + query := &m.GetTeamByIdQuery{Id: groupId} + err = GetTeamById(query) + So(err, ShouldEqual, m.ErrTeamNotFound) + + permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1} + err = GetDashboardAclInfoList(permQuery) + So(err, ShouldBeNil) + + So(len(permQuery.Result), ShouldEqual, 0) + }) + }) + }) +} diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 669f655a159..73ea07f031f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -442,7 +442,7 @@ func DeleteUser(cmd *m.DeleteUserCommand) error { "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 = ?", + "DELETE FROM team_member WHERE user_id = ?", } for _, sql := range deletes { diff --git a/pkg/services/sqlstore/user_group.go b/pkg/services/sqlstore/user_group.go deleted file mode 100644 index c90141310ab..00000000000 --- a/pkg/services/sqlstore/user_group.go +++ /dev/null @@ -1,233 +0,0 @@ -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 -} diff --git a/pkg/services/sqlstore/user_group_test.go b/pkg/services/sqlstore/user_group_test.go deleted file mode 100644 index 76056f98837..00000000000 --- a/pkg/services/sqlstore/user_group_test.go +++ /dev/null @@ -1,114 +0,0 @@ -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) - }) - }) - }) -} diff --git a/public/app/core/components/user_group_picker.ts b/public/app/core/components/team_picker.ts similarity index 67% rename from public/app/core/components/user_group_picker.ts rename to public/app/core/components/team_picker.ts index f9c91a7f0e6..2bf6b3c83b5 100644 --- a/public/app/core/components/user_group_picker.ts +++ b/public/app/core/components/team_picker.ts @@ -10,9 +10,9 @@ const template = ` `; -export class UserGroupPickerCtrl { +export class TeamPickerCtrl { group: any; - userGroupPicked: any; + teamPicked: any; debouncedSearchGroups: any; /** @ngInject */ @@ -26,34 +26,34 @@ export class UserGroupPickerCtrl { } 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 Promise.resolve(this.backendSrv.get('/api/teams/search?perpage=10&page=1&query=' + query).then(result => { + return _.map(result.teams, ug => { return {text: ug.name, value: ug}; }); })); } onChange(option) { - this.userGroupPicked({$group: option.value}); + this.teamPicked({$group: option.value}); } } -export function userGroupPicker() { +export function teamPicker() { return { restrict: 'E', template: template, - controller: UserGroupPickerCtrl, + controller: TeamPickerCtrl, bindToController: true, controllerAs: 'ctrl', scope: { - userGroupPicked: '&', + teamPicked: '&', }, link: function(scope, elem, attrs, ctrl) { - scope.$on("user-group-picker-reset", () => { + scope.$on("team-picker-reset", () => { ctrl.reset(); }); } }; } -coreModule.directive('userGroupPicker', userGroupPicker); +coreModule.directive('teamPicker', teamPicker); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 5a9b4265f8c..f43f4df3110 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -47,7 +47,7 @@ import {helpModal} from './components/help/help'; 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'; +import {teamPicker} from './components/team_picker'; import {geminiScrollbar} from './components/scroll/scroll'; import {gfPageDirective} from './components/gf_page'; import {orgSwitcher} from './components/org_switcher'; @@ -85,7 +85,7 @@ export { NavModelSrv, NavModel, userPicker, - userGroupPicker, + teamPicker, geminiScrollbar, gfPageDirective, orgSwitcher, diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index a068cd4a789..f2527f14e16 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -109,15 +109,15 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'OrgApiKeysCtrl', resolve: loadOrgBundle, }) - .when('/org/user-groups', { - templateUrl: 'public/app/features/org/partials/user_groups.html', - controller : 'UserGroupsCtrl', + .when('/org/teams', { + templateUrl: 'public/app/features/org/partials/teams.html', + controller : 'TeamsCtrl', controllerAs: 'ctrl', resolve: loadOrgBundle, }) - .when('/org/user-groups/edit/:id', { - templateUrl: 'public/app/features/org/partials/user_group_details.html', - controller : 'UserGroupDetailsCtrl', + .when('/org/teams/edit/:id', { + templateUrl: 'public/app/features/org/partials/team_details.html', + controller : 'TeamDetailsCtrl', controllerAs: 'ctrl', resolve: loadOrgBundle, }) diff --git a/public/app/features/dashboard/acl/acl.html b/public/app/features/dashboard/acl/acl.html index 8dfa86f958a..aa73f607684 100644 --- a/public/app/features/dashboard/acl/acl.html +++ b/public/app/features/dashboard/acl/acl.html @@ -52,7 +52,7 @@
- +
@@ -101,9 +101,9 @@ - + - + diff --git a/public/app/features/dashboard/acl/acl.ts b/public/app/features/dashboard/acl/acl.ts index 95fe45a40c8..55c2eb98572 100644 --- a/public/app/features/dashboard/acl/acl.ts +++ b/public/app/features/dashboard/acl/acl.ts @@ -12,7 +12,7 @@ export class AclCtrl { {value: 4, text: 'Admin'} ]; aclTypes = [ - {value: 'Group', text: 'User Group'}, + {value: 'Group', text: 'Team'}, {value: 'User', text: 'User'}, {value: 'Viewer', text: 'Everyone With Viewer Role'}, {value: 'Editor', text: 'Everyone With Editor Role'} @@ -58,10 +58,10 @@ export class AclCtrl { item.nameHtml = this.$sce.trustAsHtml(item.userLogin); item.sortName = item.userLogin; item.sortRank = 10; - } else if (item.userGroupId > 0) { + } else if (item.teamId > 0) { item.icon = "fa fa-fw fa-users"; - item.nameHtml = this.$sce.trustAsHtml(item.userGroup); - item.sortName = item.userGroup; + item.nameHtml = this.$sce.trustAsHtml(item.team); + item.sortName = item.team; item.sortRank = 20; } else if (item.role) { item.icon = "fa fa-fw fa-street-view"; @@ -89,7 +89,7 @@ export class AclCtrl { updated.push({ id: item.id, userId: item.userId, - userGroupId: item.userGroupId, + teamId: item.teamId, role: item.role, permission: item.permission, }); @@ -144,7 +144,7 @@ export class AclCtrl { 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); + (origItem.teamId && newItem.teamId && origItem.teamId === newItem.teamId); } userPicked(user) { @@ -153,8 +153,8 @@ export class AclCtrl { } groupPicked(group) { - this.addNewItem({userGroupId: group.id, userGroup: group.name, permission: 1}); - this.$scope.$broadcast('user-group-picker-reset'); + this.addNewItem({teamId: group.id, team: group.name, permission: 1}); + this.$scope.$broadcast('team-picker-reset'); } removeItem(index) { @@ -179,7 +179,7 @@ export function dashAclModal() { export interface FormModel { dashboardId: number; userId?: number; - userGroupId?: number; + teamId?: number; PermissionType: number; } @@ -189,8 +189,8 @@ export interface DashboardAcl { userId?: number; userLogin?: string; userEmail?: string; - userGroupId?: number; - userGroup?: string; + teamId?: number; + team?: string; permission?: number; permissionName?: string; role?: string; diff --git a/public/app/features/dashboard/acl/specs/acl_specs.ts b/public/app/features/dashboard/acl/specs/acl_specs.ts index 479db4b4de9..4c746ee9597 100644 --- a/public/app/features/dashboard/acl/specs/acl_specs.ts +++ b/public/app/features/dashboard/acl/specs/acl_specs.ts @@ -40,12 +40,12 @@ describe('AclCtrl', () => { ctx.ctrl.userPicked(userItem); - const userGroupItem = { + const teamItem = { id: 2, name: 'ug1', }; - ctx.ctrl.groupPicked(userGroupItem); + ctx.ctrl.groupPicked(teamItem); ctx.ctrl.newType = 'Editor'; ctx.ctrl.typeChanged(); @@ -54,10 +54,10 @@ describe('AclCtrl', () => { ctx.ctrl.typeChanged(); }); - it('should sort the result by role, user group and user', () => { + it('should sort the result by role, team 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[2].teamId).to.eql(2); expect(ctx.ctrl.items[3].userId).to.eql(2); }); @@ -71,7 +71,7 @@ describe('AclCtrl', () => { 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].teamId).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); @@ -124,19 +124,19 @@ describe('AclCtrl', () => { }); }); - describe('when duplicate user group permissions are added', () => { + describe('when duplicate team permissions are added', () => { beforeEach(() => { backendSrv.get.reset(); backendSrv.post.reset(); ctx.ctrl.items = []; - const userGroupItem = { + const teamItem = { id: 2, name: 'ug1', }; - ctx.ctrl.groupPicked(userGroupItem); - ctx.ctrl.groupPicked(userGroupItem); + ctx.ctrl.groupPicked(teamItem); + ctx.ctrl.groupPicked(teamItem); }); it('should throw a validation error', () => { @@ -148,25 +148,25 @@ describe('AclCtrl', () => { }); }); - describe('when one inherited and one not inherited user group permission are added', () => { + describe('when one inherited and one not inherited team permission are added', () => { beforeEach(() => { backendSrv.get.reset(); backendSrv.post.reset(); ctx.ctrl.items = []; - const inheritedUserGroupItem = { + const inheritedTeamItem = { id: 2, name: 'ug1', dashboardId: -1 }; - ctx.ctrl.items.push(inheritedUserGroupItem); + ctx.ctrl.items.push(inheritedTeamItem); - const userGroupItem = { + const teamItem = { id: 2, name: 'ug1', }; - ctx.ctrl.groupPicked(userGroupItem); + ctx.ctrl.groupPicked(teamItem); }); it('should not throw a validation error', () => { diff --git a/public/app/features/org/all.ts b/public/app/features/org/all.ts index a1106faf061..11a362d728a 100644 --- a/public/app/features/org/all.ts +++ b/public/app/features/org/all.ts @@ -5,7 +5,9 @@ import './select_org_ctrl'; import './change_password_ctrl'; import './new_org_ctrl'; import './user_invite_ctrl'; -import './user_groups_ctrl'; +import './teams_ctrl'; +import './team_details_ctrl'; +import './create_team_modal'; import './org_api_keys_ctrl'; import './org_details_ctrl'; import './prefs_control'; diff --git a/public/app/features/org/create_team_modal.ts b/public/app/features/org/create_team_modal.ts new file mode 100644 index 00000000000..8c82dbf3d9f --- /dev/null +++ b/public/app/features/org/create_team_modal.ts @@ -0,0 +1,37 @@ +/// + +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +export class CreateTeamCtrl { + teamName = ''; + + /** @ngInject */ + constructor(private backendSrv, private $location) { + } + + createTeam() { + this.backendSrv.post('/api/teams', {name: this.teamName}).then((result) => { + if (result.teamId) { + this.$location.path('/org/teams/edit/' + result.teamId); + } + this.dismiss(); + }); + } + + dismiss() { + appEvents.emit('hide-modal'); + } +} + +export function createTeamModal() { + return { + restrict: 'E', + templateUrl: 'public/app/features/org/partials/create_team.html', + controller: CreateTeamCtrl, + bindToController: true, + controllerAs: 'ctrl', + }; +} + +coreModule.directive('createTeamModal', createTeamModal); diff --git a/public/app/features/org/create_user_group_modal.ts b/public/app/features/org/create_user_group_modal.ts deleted file mode 100644 index e4be011450d..00000000000 --- a/public/app/features/org/create_user_group_modal.ts +++ /dev/null @@ -1,37 +0,0 @@ -/// - -import coreModule from 'app/core/core_module'; -import appEvents from 'app/core/app_events'; - -export class CreateUserGroupCtrl { - userGroupName = ''; - - /** @ngInject */ - constructor(private backendSrv, 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); diff --git a/public/app/features/org/partials/create_user_group.html b/public/app/features/org/partials/create_team.html similarity index 68% rename from public/app/features/org/partials/create_user_group.html rename to public/app/features/org/partials/create_team.html index 1ef0ef2822e..02c48f4ef68 100644 --- a/public/app/features/org/partials/create_user_group.html +++ b/public/app/features/org/partials/create_team.html @@ -1,7 +1,7 @@