mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #15977 from grafana/admin-on-create-poc
Editors becomes admin when creating dashboards, folders & teams
This commit is contained in:
commit
f2b06a89f2
@ -259,7 +259,7 @@ external_manage_info =
|
|||||||
viewers_can_edit = false
|
viewers_can_edit = false
|
||||||
|
|
||||||
# Editors can administrate dashboard, folders and teams they create
|
# Editors can administrate dashboard, folders and teams they create
|
||||||
editors_can_own = false
|
editors_can_admin = false
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
# Login cookie name
|
# Login cookie name
|
||||||
|
@ -239,7 +239,7 @@ log_queries =
|
|||||||
;viewers_can_edit = false
|
;viewers_can_edit = false
|
||||||
|
|
||||||
# Editors can administrate dashboard, folders and teams they create
|
# Editors can administrate dashboard, folders and teams they create
|
||||||
;editors_can_own = false
|
;editors_can_admin = false
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
# Login cookie name
|
# Login cookie name
|
||||||
|
@ -354,6 +354,11 @@ options are `Admin` and `Editor`. e.g. :
|
|||||||
Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
|
Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
|
||||||
Defaults to `false`.
|
Defaults to `false`.
|
||||||
|
|
||||||
|
### editors_can_admin
|
||||||
|
|
||||||
|
Editors can administrate dashboards, folders and teams they create.
|
||||||
|
Defaults to `false`.
|
||||||
|
|
||||||
### login_hint
|
### login_hint
|
||||||
|
|
||||||
Text used as placeholder text on login page for login/username input.
|
Text used as placeholder text on login page for login/username input.
|
||||||
|
@ -28,6 +28,9 @@ Can do everything scoped to the organization. For example:
|
|||||||
- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards.
|
- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards.
|
||||||
- **Cannot** create or edit data sources nor invite new users.
|
- **Cannot** create or edit data sources nor invite new users.
|
||||||
|
|
||||||
|
This role can be tweaked via Grafana server setting [editors_can_admin]({{< relref "installation/configuration.md#editors_can_admin" >}}). If you set this to true users
|
||||||
|
with **Editor** can also administrate dashboards, folders and teams they create. Useful for enabling self organizing teams.
|
||||||
|
|
||||||
## Viewer Role
|
## Viewer Role
|
||||||
|
|
||||||
- View any dashboard. This can be disabled on specific folders and dashboards.
|
- View any dashboard. This can be disabled on specific folders and dashboards.
|
||||||
|
@ -2,6 +2,7 @@ import React, { PureComponent, SyntheticEvent } from 'react';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onConfirm(): void;
|
onConfirm(): void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -33,25 +34,22 @@ export class DeleteButton extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onConfirm } = this.props;
|
const { onConfirm, disabled } = this.props;
|
||||||
let showConfirm;
|
const showConfirmClass = this.state.showConfirm ? 'show' : 'hide';
|
||||||
let showDeleteButton;
|
const showDeleteButtonClass = this.state.showConfirm ? 'hide' : 'show';
|
||||||
|
const disabledClass = disabled ? 'disabled btn-inverse' : '';
|
||||||
if (this.state.showConfirm) {
|
const onClick = disabled ? () => {} : this.onClickDelete;
|
||||||
showConfirm = 'show';
|
|
||||||
showDeleteButton = 'hide';
|
|
||||||
} else {
|
|
||||||
showConfirm = 'hide';
|
|
||||||
showDeleteButton = 'show';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="delete-button-container">
|
<span className="delete-button-container">
|
||||||
<a className={'delete-button ' + showDeleteButton + ' btn btn-danger btn-small'} onClick={this.onClickDelete}>
|
<a
|
||||||
|
className={`delete-button ${showDeleteButtonClass} btn btn-danger btn-small ${disabledClass}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<i className="fa fa-remove" />
|
<i className="fa fa-remove" />
|
||||||
</a>
|
</a>
|
||||||
<span className="confirm-delete-container">
|
<span className="confirm-delete-container">
|
||||||
<span className={'confirm-delete ' + showConfirm}>
|
<span className={`confirm-delete ${showConfirmClass}`}>
|
||||||
<a className="btn btn-small" onClick={this.onClickCancel}>
|
<a className="btn btn-small" onClick={this.onClickCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
|
@ -14,6 +14,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
|
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
|
||||||
reqEditorRole := middleware.ReqEditorRole
|
reqEditorRole := middleware.ReqEditorRole
|
||||||
reqOrgAdmin := middleware.ReqOrgAdmin
|
reqOrgAdmin := middleware.ReqOrgAdmin
|
||||||
|
reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin)
|
||||||
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
|
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
|
||||||
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
|
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
|
||||||
quota := middleware.Quota(hs.QuotaService)
|
quota := middleware.Quota(hs.QuotaService)
|
||||||
@ -41,8 +42,8 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/org/users", reqOrgAdmin, hs.Index)
|
r.Get("/org/users", reqOrgAdmin, hs.Index)
|
||||||
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
|
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
|
||||||
r.Get("/org/users/invite", reqOrgAdmin, hs.Index)
|
r.Get("/org/users/invite", reqOrgAdmin, hs.Index)
|
||||||
r.Get("/org/teams", reqOrgAdmin, hs.Index)
|
r.Get("/org/teams", reqCanAccessTeams, hs.Index)
|
||||||
r.Get("/org/teams/*", reqOrgAdmin, hs.Index)
|
r.Get("/org/teams/*", reqCanAccessTeams, hs.Index)
|
||||||
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
|
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
|
||||||
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
|
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
|
||||||
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
|
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
|
||||||
@ -153,20 +154,21 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
// team (admin permission required)
|
// team (admin permission required)
|
||||||
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
|
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
|
||||||
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(CreateTeam))
|
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(hs.CreateTeam))
|
||||||
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam))
|
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(hs.UpdateTeam))
|
||||||
teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID))
|
teamsRoute.Delete("/:teamId", Wrap(hs.DeleteTeamByID))
|
||||||
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
|
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
|
||||||
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember))
|
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(hs.AddTeamMember))
|
||||||
teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember))
|
teamsRoute.Put("/:teamId/members/:userId", bind(m.UpdateTeamMemberCommand{}), Wrap(hs.UpdateTeamMember))
|
||||||
teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences))
|
teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember))
|
||||||
teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences))
|
teamsRoute.Get("/:teamId/preferences", Wrap(hs.GetTeamPreferences))
|
||||||
}, reqOrgAdmin)
|
teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(hs.UpdateTeamPreferences))
|
||||||
|
}, reqCanAccessTeams)
|
||||||
|
|
||||||
// team without requirement of user to be org admin
|
// team without requirement of user to be org admin
|
||||||
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
|
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
|
||||||
teamsRoute.Get("/:teamId", Wrap(GetTeamByID))
|
teamsRoute.Get("/:teamId", Wrap(GetTeamByID))
|
||||||
teamsRoute.Get("/search", Wrap(SearchTeams))
|
teamsRoute.Get("/search", Wrap(hs.SearchTeams))
|
||||||
})
|
})
|
||||||
|
|
||||||
// org information available to all users.
|
// org information available to all users.
|
||||||
@ -265,7 +267,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||||
folderRoute.Get("/", Wrap(GetFolders))
|
folderRoute.Get("/", Wrap(GetFolders))
|
||||||
folderRoute.Get("/id/:id", Wrap(GetFolderByID))
|
folderRoute.Get("/id/:id", Wrap(GetFolderByID))
|
||||||
folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(CreateFolder))
|
folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(hs.CreateFolder))
|
||||||
|
|
||||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||||
folderUidRoute.Get("/", Wrap(GetFolderByUID))
|
folderUidRoute.Get("/", Wrap(GetFolderByUID))
|
||||||
|
@ -213,7 +213,8 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand)
|
|||||||
|
|
||||||
dash := cmd.GetDashboardModel()
|
dash := cmd.GetDashboardModel()
|
||||||
|
|
||||||
if dash.Id == 0 && dash.Uid == "" {
|
newDashboard := dash.Id == 0 && dash.Uid == ""
|
||||||
|
if newDashboard {
|
||||||
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
|
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Error(500, "failed to get quota", err)
|
return Error(500, "failed to get quota", err)
|
||||||
@ -276,6 +277,15 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand)
|
|||||||
return Error(500, "Failed to save dashboard", err)
|
return Error(500, "Failed to save dashboard", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hs.Cfg.EditorsCanAdmin && newDashboard {
|
||||||
|
inFolder := cmd.FolderId > 0
|
||||||
|
err := dashboards.MakeUserAdmin(hs.Bus, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
|
||||||
|
if err != nil {
|
||||||
|
hs.log.Error("Could not make user admin", "dashboard", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err)
|
||||||
|
return Error(500, "Failed to make user admin of dashboard", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||||
return JSON(200, util.DynMap{
|
return JSON(200, util.DynMap{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
@ -974,6 +974,7 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
|
|||||||
|
|
||||||
hs := HTTPServer{
|
hs := HTTPServer{
|
||||||
Bus: bus.GetBus(),
|
Bus: bus.GetBus(),
|
||||||
|
Cfg: setting.NewCfg(),
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := setupScenarioContext(url)
|
sc := setupScenarioContext(url)
|
||||||
@ -1024,6 +1025,7 @@ func restoreDashboardVersionScenario(desc string, url string, routePattern strin
|
|||||||
defer bus.ClearBusHandlers()
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
hs := HTTPServer{
|
hs := HTTPServer{
|
||||||
|
Cfg: setting.NewCfg(),
|
||||||
Bus: bus.GetBus(),
|
Bus: bus.GetBus(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,13 +54,20 @@ func GetFolderByID(c *m.ReqContext) Response {
|
|||||||
return JSON(200, toFolderDto(g, folder))
|
return JSON(200, toFolderDto(g, folder))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
|
func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
|
||||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||||
err := s.CreateFolder(&cmd)
|
err := s.CreateFolder(&cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toFolderError(err)
|
return toFolderError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hs.Cfg.EditorsCanAdmin {
|
||||||
|
if err := dashboards.MakeUserAdmin(hs.Bus, c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil {
|
||||||
|
hs.log.Error("Could not make user admin", "folder", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err)
|
||||||
|
return Error(500, "Failed to make user admin of folder", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
|
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
|
||||||
return JSON(200, toFolderDto(g, cmd.Result))
|
return JSON(200, toFolderDto(g, cmd.Result))
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
@ -141,12 +142,17 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa
|
|||||||
Convey(desc+" "+url, func() {
|
Convey(desc+" "+url, func() {
|
||||||
defer bus.ClearBusHandlers()
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
Cfg: setting.NewCfg(),
|
||||||
|
}
|
||||||
|
|
||||||
sc := setupScenarioContext(url)
|
sc := setupScenarioContext(url)
|
||||||
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
sc.context = c
|
sc.context = c
|
||||||
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
|
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
|
||||||
|
|
||||||
return CreateFolder(c, cmd)
|
return hs.CreateFolder(c, cmd)
|
||||||
})
|
})
|
||||||
|
|
||||||
origNewFolderService := dashboards.NewFolderService
|
origNewFolderService := dashboards.NewFolderService
|
||||||
|
@ -167,7 +167,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
|
|||||||
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
||||||
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
||||||
"viewersCanEdit": setting.ViewersCanEdit,
|
"viewersCanEdit": setting.ViewersCanEdit,
|
||||||
"editorsCanOwn": hs.Cfg.EditorsCanOwn,
|
"editorsCanAdmin": hs.Cfg.EditorsCanAdmin,
|
||||||
"disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
|
"disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
|
||||||
"buildInfo": map[string]interface{}{
|
"buildInfo": map[string]interface{}{
|
||||||
"version": setting.BuildVersion,
|
"version": setting.BuildVersion,
|
||||||
|
@ -327,6 +327,27 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_VIEWER) && hs.Cfg.EditorsCanAdmin {
|
||||||
|
cfgNode := &dtos.NavLink{
|
||||||
|
Id: "cfg",
|
||||||
|
Text: "Configuration",
|
||||||
|
SubTitle: "Organization: " + c.OrgName,
|
||||||
|
Icon: "gicon gicon-cog",
|
||||||
|
Url: setting.AppSubUrl + "/org/teams",
|
||||||
|
Children: []*dtos.NavLink{
|
||||||
|
{
|
||||||
|
Text: "Teams",
|
||||||
|
Id: "teams",
|
||||||
|
Description: "Manage org groups",
|
||||||
|
Icon: "gicon gicon-team",
|
||||||
|
Url: setting.AppSubUrl + "/org/teams",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data.NavTree = append(data.NavTree, cfgNode)
|
||||||
|
}
|
||||||
|
|
||||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||||
Text: "Help",
|
Text: "Help",
|
||||||
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
|
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
|
||||||
|
@ -4,19 +4,38 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/teamguardian"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// POST /api/teams
|
// POST /api/teams
|
||||||
func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
|
func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
|
||||||
|
if c.OrgRole == m.ROLE_VIEWER {
|
||||||
|
return Error(403, "Not allowed to create team.", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hs.Bus.Dispatch(&cmd); err != nil {
|
||||||
if err == m.ErrTeamNameTaken {
|
if err == m.ErrTeamNameTaken {
|
||||||
return Error(409, "Team name taken", err)
|
return Error(409, "Team name taken", err)
|
||||||
}
|
}
|
||||||
return Error(500, "Failed to create Team", err)
|
return Error(500, "Failed to create Team", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanAdmin {
|
||||||
|
addMemberCmd := m.AddTeamMemberCommand{
|
||||||
|
UserId: c.SignedInUser.UserId,
|
||||||
|
OrgId: cmd.OrgId,
|
||||||
|
TeamId: cmd.Result.Id,
|
||||||
|
Permission: m.PERMISSION_ADMIN,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hs.Bus.Dispatch(&addMemberCmd); err != nil {
|
||||||
|
c.Logger.Error("Could not add creator to team.", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return JSON(200, &util.DynMap{
|
return JSON(200, &util.DynMap{
|
||||||
"teamId": cmd.Result.Id,
|
"teamId": cmd.Result.Id,
|
||||||
"message": "Team created",
|
"message": "Team created",
|
||||||
@ -24,10 +43,15 @@ func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PUT /api/teams/:teamId
|
// PUT /api/teams/:teamId
|
||||||
func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
|
func (hs *HTTPServer) UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
cmd.Id = c.ParamsInt64(":teamId")
|
cmd.Id = c.ParamsInt64(":teamId")
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
|
||||||
|
if err := teamguardian.CanAdmin(hs.Bus, cmd.OrgId, cmd.Id, c.SignedInUser); err != nil {
|
||||||
|
return Error(403, "Not allowed to update team", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hs.Bus.Dispatch(&cmd); err != nil {
|
||||||
if err == m.ErrTeamNameTaken {
|
if err == m.ErrTeamNameTaken {
|
||||||
return Error(400, "Team name taken", err)
|
return Error(400, "Team name taken", err)
|
||||||
}
|
}
|
||||||
@ -38,18 +62,26 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DELETE /api/teams/:teamId
|
// DELETE /api/teams/:teamId
|
||||||
func DeleteTeamByID(c *m.ReqContext) Response {
|
func (hs *HTTPServer) DeleteTeamByID(c *m.ReqContext) Response {
|
||||||
if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
|
orgId := c.OrgId
|
||||||
|
teamId := c.ParamsInt64(":teamId")
|
||||||
|
user := c.SignedInUser
|
||||||
|
|
||||||
|
if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, user); err != nil {
|
||||||
|
return Error(403, "Not allowed to delete team", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hs.Bus.Dispatch(&m.DeleteTeamCommand{OrgId: orgId, Id: teamId}); err != nil {
|
||||||
if err == m.ErrTeamNotFound {
|
if err == m.ErrTeamNotFound {
|
||||||
return Error(404, "Failed to delete Team. ID not found", nil)
|
return Error(404, "Failed to delete Team. ID not found", nil)
|
||||||
}
|
}
|
||||||
return Error(500, "Failed to update Team", err)
|
return Error(500, "Failed to delete Team", err)
|
||||||
}
|
}
|
||||||
return Success("Team deleted")
|
return Success("Team deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/teams/search
|
// GET /api/teams/search
|
||||||
func SearchTeams(c *m.ReqContext) Response {
|
func (hs *HTTPServer) SearchTeams(c *m.ReqContext) Response {
|
||||||
perPage := c.QueryInt("perpage")
|
perPage := c.QueryInt("perpage")
|
||||||
if perPage <= 0 {
|
if perPage <= 0 {
|
||||||
perPage = 1000
|
perPage = 1000
|
||||||
@ -59,12 +91,18 @@ func SearchTeams(c *m.ReqContext) Response {
|
|||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userIdFilter int64
|
||||||
|
if hs.Cfg.EditorsCanAdmin && c.OrgRole != m.ROLE_ADMIN {
|
||||||
|
userIdFilter = c.SignedInUser.UserId
|
||||||
|
}
|
||||||
|
|
||||||
query := m.SearchTeamsQuery{
|
query := m.SearchTeamsQuery{
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
Query: c.Query("query"),
|
Query: c.Query("query"),
|
||||||
Name: c.Query("name"),
|
Name: c.Query("name"),
|
||||||
Page: page,
|
UserIdFilter: userIdFilter,
|
||||||
Limit: perPage,
|
Page: page,
|
||||||
|
Limit: perPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
@ -98,11 +136,25 @@ func GetTeamByID(c *m.ReqContext) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/teams/:teamId/preferences
|
// GET /api/teams/:teamId/preferences
|
||||||
func GetTeamPreferences(c *m.ReqContext) Response {
|
func (hs *HTTPServer) GetTeamPreferences(c *m.ReqContext) Response {
|
||||||
return getPreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"))
|
teamId := c.ParamsInt64(":teamId")
|
||||||
|
orgId := c.OrgId
|
||||||
|
|
||||||
|
if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil {
|
||||||
|
return Error(403, "Not allowed to view team preferences.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPreferencesFor(orgId, 0, teamId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT /api/teams/:teamId/preferences
|
// PUT /api/teams/:teamId/preferences
|
||||||
func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
|
func (hs *HTTPServer) UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
|
||||||
return updatePreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"), &dtoCmd)
|
teamId := c.ParamsInt64(":teamId")
|
||||||
|
orgId := c.OrgId
|
||||||
|
|
||||||
|
if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil {
|
||||||
|
return Error(403, "Not allowed to update team preferences.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatePreferencesFor(orgId, 0, teamId, &dtoCmd)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/teamguardian"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
@ -29,11 +30,15 @@ func GetTeamMembers(c *m.ReqContext) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/teams/:teamId/members
|
// POST /api/teams/:teamId/members
|
||||||
func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
|
func (hs *HTTPServer) AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
|
||||||
cmd.TeamId = c.ParamsInt64(":teamId")
|
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
|
cmd.TeamId = c.ParamsInt64(":teamId")
|
||||||
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err := teamguardian.CanAdmin(hs.Bus, cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil {
|
||||||
|
return Error(403, "Not allowed to add team member", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hs.Bus.Dispatch(&cmd); err != nil {
|
||||||
if err == m.ErrTeamNotFound {
|
if err == m.ErrTeamNotFound {
|
||||||
return Error(404, "Team not found", nil)
|
return Error(404, "Team not found", nil)
|
||||||
}
|
}
|
||||||
@ -50,9 +55,48 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUT /:teamId/members/:userId
|
||||||
|
func (hs *HTTPServer) UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response {
|
||||||
|
teamId := c.ParamsInt64(":teamId")
|
||||||
|
orgId := c.OrgId
|
||||||
|
|
||||||
|
if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil {
|
||||||
|
return Error(403, "Not allowed to update team member", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.OrgRole != m.ROLE_ADMIN {
|
||||||
|
cmd.ProtectLastAdmin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.TeamId = teamId
|
||||||
|
cmd.UserId = c.ParamsInt64(":userId")
|
||||||
|
cmd.OrgId = orgId
|
||||||
|
|
||||||
|
if err := hs.Bus.Dispatch(&cmd); err != nil {
|
||||||
|
if err == m.ErrTeamMemberNotFound {
|
||||||
|
return Error(404, "Team member not found.", nil)
|
||||||
|
}
|
||||||
|
return Error(500, "Failed to update team member.", err)
|
||||||
|
}
|
||||||
|
return Success("Team member updated")
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE /api/teams/:teamId/members/:userId
|
// DELETE /api/teams/:teamId/members/:userId
|
||||||
func RemoveTeamMember(c *m.ReqContext) Response {
|
func (hs *HTTPServer) RemoveTeamMember(c *m.ReqContext) Response {
|
||||||
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
|
orgId := c.OrgId
|
||||||
|
teamId := c.ParamsInt64(":teamId")
|
||||||
|
userId := c.ParamsInt64(":userId")
|
||||||
|
|
||||||
|
if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil {
|
||||||
|
return Error(403, "Not allowed to remove team member", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protectLastAdmin := false
|
||||||
|
if c.OrgRole != m.ROLE_ADMIN {
|
||||||
|
protectLastAdmin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hs.Bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: orgId, TeamId: teamId, UserId: userId, ProtectLastAdmin: protectLastAdmin}); err != nil {
|
||||||
if err == m.ErrTeamNotFound {
|
if err == m.ErrTeamNotFound {
|
||||||
return Error(404, "Team not found", nil)
|
return Error(404, "Team not found", nil)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
@ -20,6 +22,10 @@ func TestTeamApiEndpoint(t *testing.T) {
|
|||||||
TotalCount: 2,
|
TotalCount: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hs := &HTTPServer{
|
||||||
|
Cfg: setting.NewCfg(),
|
||||||
|
}
|
||||||
|
|
||||||
Convey("When searching with no parameters", func() {
|
Convey("When searching with no parameters", func() {
|
||||||
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
|
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
|
||||||
var sentLimit int
|
var sentLimit int
|
||||||
@ -33,7 +39,7 @@ func TestTeamApiEndpoint(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.handlerFunc = SearchTeams
|
sc.handlerFunc = hs.SearchTeams
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
So(sentLimit, ShouldEqual, 1000)
|
So(sentLimit, ShouldEqual, 1000)
|
||||||
@ -60,7 +66,7 @@ func TestTeamApiEndpoint(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.handlerFunc = SearchTeams
|
sc.handlerFunc = hs.SearchTeams
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||||
|
|
||||||
So(sentLimit, ShouldEqual, 10)
|
So(sentLimit, ShouldEqual, 10)
|
||||||
|
@ -86,3 +86,20 @@ func Auth(options *AuthOptions) macaron.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminOrFeatureEnabled creates a middleware that allows access
|
||||||
|
// if the signed in user is either an Org Admin or if the
|
||||||
|
// feature flag is enabled.
|
||||||
|
// Intended for when feature flags open up access to APIs that
|
||||||
|
// are otherwise only available to admins.
|
||||||
|
func AdminOrFeatureEnabled(enabled bool) macaron.Handler {
|
||||||
|
return func(c *m.ReqContext) {
|
||||||
|
if c.OrgRole == m.ROLE_ADMIN {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enabled {
|
||||||
|
accessForbidden(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,9 +7,12 @@ import (
|
|||||||
|
|
||||||
// Typed errors
|
// Typed errors
|
||||||
var (
|
var (
|
||||||
ErrTeamNotFound = errors.New("Team not found")
|
ErrTeamNotFound = errors.New("Team not found")
|
||||||
ErrTeamNameTaken = errors.New("Team name is taken")
|
ErrTeamNameTaken = errors.New("Team name is taken")
|
||||||
ErrTeamMemberNotFound = errors.New("Team member not found")
|
ErrTeamMemberNotFound = errors.New("Team member not found")
|
||||||
|
ErrLastTeamAdmin = errors.New("Not allowed to remove last admin")
|
||||||
|
ErrNotAllowedToUpdateTeam = errors.New("User not allowed to update team")
|
||||||
|
ErrNotAllowedToUpdateTeamInDifferentOrg = errors.New("User not allowed to update team in another org")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Team model
|
// Team model
|
||||||
@ -59,22 +62,24 @@ type GetTeamsByUserQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SearchTeamsQuery struct {
|
type SearchTeamsQuery struct {
|
||||||
Query string
|
Query string
|
||||||
Name string
|
Name string
|
||||||
Limit int
|
Limit int
|
||||||
Page int
|
Page int
|
||||||
OrgId int64
|
OrgId int64
|
||||||
|
UserIdFilter int64
|
||||||
|
|
||||||
Result SearchTeamQueryResult
|
Result SearchTeamQueryResult
|
||||||
}
|
}
|
||||||
|
|
||||||
type TeamDTO struct {
|
type TeamDTO struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
OrgId int64 `json:"orgId"`
|
OrgId int64 `json:"orgId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
AvatarUrl string `json:"avatarUrl"`
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
MemberCount int64 `json:"memberCount"`
|
MemberCount int64 `json:"memberCount"`
|
||||||
|
Permission PermissionType `json:"permission"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchTeamQueryResult struct {
|
type SearchTeamQueryResult struct {
|
||||||
|
@ -12,11 +12,12 @@ var (
|
|||||||
|
|
||||||
// TeamMember model
|
// TeamMember model
|
||||||
type TeamMember struct {
|
type TeamMember struct {
|
||||||
Id int64
|
Id int64
|
||||||
OrgId int64
|
OrgId int64
|
||||||
TeamId int64
|
TeamId int64
|
||||||
UserId int64
|
UserId int64
|
||||||
External bool
|
External bool // Signals that the membership has been created by an external systems, such as LDAP
|
||||||
|
Permission PermissionType
|
||||||
|
|
||||||
Created time.Time
|
Created time.Time
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
@ -26,16 +27,26 @@ type TeamMember struct {
|
|||||||
// COMMANDS
|
// COMMANDS
|
||||||
|
|
||||||
type AddTeamMemberCommand struct {
|
type AddTeamMemberCommand struct {
|
||||||
UserId int64 `json:"userId" binding:"Required"`
|
UserId int64 `json:"userId" binding:"Required"`
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
TeamId int64 `json:"-"`
|
TeamId int64 `json:"-"`
|
||||||
External bool `json:"-"`
|
External bool `json:"-"`
|
||||||
|
Permission PermissionType `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTeamMemberCommand struct {
|
||||||
|
UserId int64 `json:"-"`
|
||||||
|
OrgId int64 `json:"-"`
|
||||||
|
TeamId int64 `json:"-"`
|
||||||
|
Permission PermissionType `json:"permission"`
|
||||||
|
ProtectLastAdmin bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoveTeamMemberCommand struct {
|
type RemoveTeamMemberCommand struct {
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
UserId int64
|
UserId int64
|
||||||
TeamId int64
|
TeamId int64
|
||||||
|
ProtectLastAdmin bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------
|
// ----------------------
|
||||||
@ -53,12 +64,13 @@ type GetTeamMembersQuery struct {
|
|||||||
// Projections and DTOs
|
// Projections and DTOs
|
||||||
|
|
||||||
type TeamMemberDTO struct {
|
type TeamMemberDTO struct {
|
||||||
OrgId int64 `json:"orgId"`
|
OrgId int64 `json:"orgId"`
|
||||||
TeamId int64 `json:"teamId"`
|
TeamId int64 `json:"teamId"`
|
||||||
UserId int64 `json:"userId"`
|
UserId int64 `json:"userId"`
|
||||||
External bool `json:"-"`
|
External bool `json:"-"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Login string `json:"login"`
|
Login string `json:"login"`
|
||||||
AvatarUrl string `json:"avatarUrl"`
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
|
Permission PermissionType `json:"permission"`
|
||||||
}
|
}
|
||||||
|
55
pkg/services/dashboards/acl_service.go
Normal file
55
pkg/services/dashboards/acl_service.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package dashboards
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeUserAdmin(bus bus.Bus, orgId int64, userId int64, dashboardId int64, setViewAndEditPermissions bool) error {
|
||||||
|
rtEditor := models.ROLE_EDITOR
|
||||||
|
rtViewer := models.ROLE_VIEWER
|
||||||
|
|
||||||
|
items := []*models.DashboardAcl{
|
||||||
|
{
|
||||||
|
OrgId: orgId,
|
||||||
|
DashboardId: dashboardId,
|
||||||
|
UserId: userId,
|
||||||
|
Permission: models.PERMISSION_ADMIN,
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if setViewAndEditPermissions {
|
||||||
|
items = append(items,
|
||||||
|
&models.DashboardAcl{
|
||||||
|
OrgId: orgId,
|
||||||
|
DashboardId: dashboardId,
|
||||||
|
Role: &rtEditor,
|
||||||
|
Permission: models.PERMISSION_EDIT,
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
},
|
||||||
|
&models.DashboardAcl{
|
||||||
|
OrgId: orgId,
|
||||||
|
DashboardId: dashboardId,
|
||||||
|
Role: &rtViewer,
|
||||||
|
Permission: models.PERMISSION_VIEW,
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
aclCmd := &models.UpdateDashboardAclCommand{
|
||||||
|
DashboardId: dashboardId,
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(aclCmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -54,4 +54,8 @@ func addTeamMigrations(mg *Migrator) {
|
|||||||
mg.AddMigration("Add column external to team_member table", NewAddColumnMigration(teamMemberV1, &Column{
|
mg.AddMigration("Add column external to team_member table", NewAddColumnMigration(teamMemberV1, &Column{
|
||||||
Name: "external", Type: DB_Bool, Nullable: true,
|
Name: "external", Type: DB_Bool, Nullable: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Add column permission to team_member table", NewAddColumnMigration(teamMemberV1, &Column{
|
||||||
|
Name: "permission", Type: DB_SmallInt, Nullable: true,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,23 @@ func init() {
|
|||||||
bus.AddHandler("sql", GetTeamsByUser)
|
bus.AddHandler("sql", GetTeamsByUser)
|
||||||
|
|
||||||
bus.AddHandler("sql", AddTeamMember)
|
bus.AddHandler("sql", AddTeamMember)
|
||||||
|
bus.AddHandler("sql", UpdateTeamMember)
|
||||||
bus.AddHandler("sql", RemoveTeamMember)
|
bus.AddHandler("sql", RemoveTeamMember)
|
||||||
bus.AddHandler("sql", GetTeamMembers)
|
bus.AddHandler("sql", GetTeamMembers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTeamSearchSqlBase() string {
|
||||||
|
return `SELECT
|
||||||
|
team.id as id,
|
||||||
|
team.org_id,
|
||||||
|
team.name as name,
|
||||||
|
team.email as email,
|
||||||
|
(SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count,
|
||||||
|
team_member.permission
|
||||||
|
FROM team as team
|
||||||
|
INNER JOIN team_member on team.id = team_member.team_id AND team_member.user_id = ? `
|
||||||
|
}
|
||||||
|
|
||||||
func getTeamSelectSqlBase() string {
|
func getTeamSelectSqlBase() string {
|
||||||
return `SELECT
|
return `SELECT
|
||||||
team.id as id,
|
team.id as id,
|
||||||
@ -91,10 +104,8 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
|
|||||||
// DeleteTeam will delete a team, its member and any permissions connected to the team
|
// DeleteTeam will delete a team, its member and any permissions connected to the team
|
||||||
func DeleteTeam(cmd *m.DeleteTeamCommand) error {
|
func DeleteTeam(cmd *m.DeleteTeamCommand) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
if teamExists, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil {
|
if _, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !teamExists {
|
|
||||||
return m.ErrTeamNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deletes := []string{
|
deletes := []string{
|
||||||
@ -117,7 +128,7 @@ func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) {
|
|||||||
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil {
|
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if len(res) != 1 {
|
} else if len(res) != 1 {
|
||||||
return false, nil
|
return false, m.ErrTeamNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
@ -147,7 +158,12 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
|
|||||||
var sql bytes.Buffer
|
var sql bytes.Buffer
|
||||||
params := make([]interface{}, 0)
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
sql.WriteString(getTeamSelectSqlBase())
|
if query.UserIdFilter > 0 {
|
||||||
|
sql.WriteString(getTeamSearchSqlBase())
|
||||||
|
params = append(params, query.UserIdFilter)
|
||||||
|
} else {
|
||||||
|
sql.WriteString(getTeamSelectSqlBase())
|
||||||
|
}
|
||||||
sql.WriteString(` WHERE team.org_id = ?`)
|
sql.WriteString(` WHERE team.org_id = ?`)
|
||||||
|
|
||||||
params = append(params, query.OrgId)
|
params = append(params, query.OrgId)
|
||||||
@ -233,19 +249,18 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
|
|||||||
return m.ErrTeamMemberAlreadyAdded
|
return m.ErrTeamMemberAlreadyAdded
|
||||||
}
|
}
|
||||||
|
|
||||||
if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
|
if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !teamExists {
|
|
||||||
return m.ErrTeamNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entity := m.TeamMember{
|
entity := m.TeamMember{
|
||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
TeamId: cmd.TeamId,
|
TeamId: cmd.TeamId,
|
||||||
UserId: cmd.UserId,
|
UserId: cmd.UserId,
|
||||||
External: cmd.External,
|
External: cmd.External,
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
|
Permission: cmd.Permission,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := sess.Insert(&entity)
|
_, err := sess.Insert(&entity)
|
||||||
@ -253,13 +268,59 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTeamMember(sess *DBSession, orgId int64, teamId int64, userId int64) (m.TeamMember, error) {
|
||||||
|
rawSql := `SELECT * FROM team_member WHERE org_id=? and team_id=? and user_id=?`
|
||||||
|
var member m.TeamMember
|
||||||
|
exists, err := sess.SQL(rawSql, orgId, teamId, userId).Get(&member)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return member, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return member, m.ErrTeamMemberNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return member, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTeamMember updates a team member
|
||||||
|
func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error {
|
||||||
|
return inTransaction(func(sess *DBSession) error {
|
||||||
|
member, err := getTeamMember(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.ProtectLastAdmin {
|
||||||
|
_, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Permission != m.PERMISSION_ADMIN { // make sure we don't get invalid permission levels in store
|
||||||
|
cmd.Permission = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
member.Permission = cmd.Permission
|
||||||
|
_, err = sess.Cols("permission").Where("org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId).Update(member)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveTeamMember removes a member from a team
|
// RemoveTeamMember removes a member from a team
|
||||||
func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
|
func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
|
if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !teamExists {
|
}
|
||||||
return m.ErrTeamNotFound
|
|
||||||
|
if cmd.ProtectLastAdmin {
|
||||||
|
_, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
|
var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
|
||||||
@ -276,6 +337,29 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isLastAdmin(sess *DBSession, orgId int64, teamId int64, userId int64) (bool, error) {
|
||||||
|
rawSql := "SELECT user_id FROM team_member WHERE org_id=? and team_id=? and permission=?"
|
||||||
|
userIds := []*int64{}
|
||||||
|
err := sess.SQL(rawSql, orgId, teamId, m.PERMISSION_ADMIN).Find(&userIds)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin := false
|
||||||
|
for _, adminId := range userIds {
|
||||||
|
if userId == *adminId {
|
||||||
|
isAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdmin && len(userIds) == 1 {
|
||||||
|
return true, m.ErrLastTeamAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetTeamMembers return a list of members for the specified team
|
// GetTeamMembers return a list of members for the specified team
|
||||||
func GetTeamMembers(query *m.GetTeamMembersQuery) error {
|
func GetTeamMembers(query *m.GetTeamMembersQuery) error {
|
||||||
query.Result = make([]*m.TeamMemberDTO, 0)
|
query.Result = make([]*m.TeamMemberDTO, 0)
|
||||||
@ -293,7 +377,7 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
|
|||||||
if query.External {
|
if query.External {
|
||||||
sess.Where("team_member.external=?", dialect.BooleanStr(true))
|
sess.Where("team_member.external=?", dialect.BooleanStr(true))
|
||||||
}
|
}
|
||||||
sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external")
|
sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external", "team_member.permission")
|
||||||
sess.Asc("user.login", "user.email")
|
sess.Asc("user.login", "user.email")
|
||||||
|
|
||||||
err := sess.Find(&query.Result)
|
err := sess.Find(&query.Result)
|
||||||
|
@ -75,6 +75,72 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
|||||||
So(q2.Result[0].External, ShouldEqual, true)
|
So(q2.Result[0].External, ShouldEqual, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Should be able to update users in a team", func() {
|
||||||
|
userId := userIds[0]
|
||||||
|
team := group1.Result
|
||||||
|
addMemberCmd := m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userId}
|
||||||
|
err = AddTeamMember(&addMemberCmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
qBeforeUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
|
||||||
|
err = GetTeamMembers(qBeforeUpdate)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0)
|
||||||
|
|
||||||
|
err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
|
||||||
|
UserId: userId,
|
||||||
|
OrgId: testOrgId,
|
||||||
|
TeamId: team.Id,
|
||||||
|
Permission: m.PERMISSION_ADMIN,
|
||||||
|
})
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
qAfterUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
|
||||||
|
err = GetTeamMembers(qAfterUpdate)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(qAfterUpdate.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should default to member permission level when updating a user with invalid permission level", func() {
|
||||||
|
userID := userIds[0]
|
||||||
|
team := group1.Result
|
||||||
|
addMemberCmd := m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userID}
|
||||||
|
err = AddTeamMember(&addMemberCmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
qBeforeUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
|
||||||
|
err = GetTeamMembers(qBeforeUpdate)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0)
|
||||||
|
|
||||||
|
invalidPermissionLevel := m.PERMISSION_EDIT
|
||||||
|
err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
|
||||||
|
UserId: userID,
|
||||||
|
OrgId: testOrgId,
|
||||||
|
TeamId: team.Id,
|
||||||
|
Permission: invalidPermissionLevel,
|
||||||
|
})
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
qAfterUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
|
||||||
|
err = GetTeamMembers(qAfterUpdate)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(qAfterUpdate.Result[0].Permission, ShouldEqual, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Shouldn't be able to update a user not in the team.", func() {
|
||||||
|
err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
|
||||||
|
UserId: 1,
|
||||||
|
OrgId: testOrgId,
|
||||||
|
TeamId: group1.Result.Id,
|
||||||
|
Permission: m.PERMISSION_ADMIN,
|
||||||
|
})
|
||||||
|
|
||||||
|
So(err, ShouldEqual, m.ErrTeamMemberNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Should be able to search for teams", func() {
|
Convey("Should be able to search for teams", func() {
|
||||||
query := &m.SearchTeamsQuery{OrgId: testOrgId, Query: "group", Page: 1}
|
query := &m.SearchTeamsQuery{OrgId: testOrgId, Query: "group", Page: 1}
|
||||||
err = SearchTeams(query)
|
err = SearchTeams(query)
|
||||||
@ -114,6 +180,33 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
|||||||
So(len(q2.Result), ShouldEqual, 0)
|
So(len(q2.Result), ShouldEqual, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("When ProtectLastAdmin is set to true", func() {
|
||||||
|
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: m.PERMISSION_ADMIN})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
Convey("A user should not be able to remove the last admin", func() {
|
||||||
|
err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true})
|
||||||
|
So(err, ShouldEqual, m.ErrLastTeamAdmin)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("A user should be able to remove an admin if there are other admins", func() {
|
||||||
|
AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN})
|
||||||
|
err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true})
|
||||||
|
So(err, ShouldEqual, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("A user should not be able to remove the admin permission for the last admin", func() {
|
||||||
|
err = UpdateTeamMember(&m.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
|
||||||
|
So(err, ShouldEqual, m.ErrLastTeamAdmin)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("A user should be able to remove the admin permission if there are other admins", func() {
|
||||||
|
AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN})
|
||||||
|
err = UpdateTeamMember(&m.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
|
||||||
|
So(err, ShouldEqual, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Should be able to remove a group with users and permissions", func() {
|
Convey("Should be able to remove a group with users and permissions", func() {
|
||||||
groupId := group2.Result.Id
|
groupId := group2.Result.Id
|
||||||
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]})
|
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]})
|
||||||
|
34
pkg/services/teamguardian/team.go
Normal file
34
pkg/services/teamguardian/team.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package teamguardian
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CanAdmin(bus bus.Bus, orgId int64, teamId int64, user *m.SignedInUser) error {
|
||||||
|
if user.OrgRole == m.ROLE_ADMIN {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.OrgId != orgId {
|
||||||
|
return m.ErrNotAllowedToUpdateTeamInDifferentOrg
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := m.GetTeamMembersQuery{
|
||||||
|
OrgId: orgId,
|
||||||
|
TeamId: teamId,
|
||||||
|
UserId: user.UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, member := range cmd.Result {
|
||||||
|
if member.UserId == user.UserId && member.Permission == m.PERMISSION_ADMIN {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.ErrNotAllowedToUpdateTeam
|
||||||
|
}
|
87
pkg/services/teamguardian/teams_test.go
Normal file
87
pkg/services/teamguardian/teams_test.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package teamguardian
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateTeam(t *testing.T) {
|
||||||
|
Convey("Updating a team", t, func() {
|
||||||
|
bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
admin := m.SignedInUser{
|
||||||
|
UserId: 1,
|
||||||
|
OrgId: 1,
|
||||||
|
OrgRole: m.ROLE_ADMIN,
|
||||||
|
}
|
||||||
|
editor := m.SignedInUser{
|
||||||
|
UserId: 2,
|
||||||
|
OrgId: 1,
|
||||||
|
OrgRole: m.ROLE_EDITOR,
|
||||||
|
}
|
||||||
|
testTeam := m.Team{
|
||||||
|
Id: 1,
|
||||||
|
OrgId: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("Given an editor and a team he isn't a member of", func() {
|
||||||
|
Convey("Should not be able to update the team", func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error {
|
||||||
|
cmd.Result = []*m.TeamMemberDTO{}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &editor)
|
||||||
|
So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given an editor and a team he is an admin in", func() {
|
||||||
|
Convey("Should be able to update the team", func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error {
|
||||||
|
cmd.Result = []*m.TeamMemberDTO{{
|
||||||
|
OrgId: testTeam.OrgId,
|
||||||
|
TeamId: testTeam.Id,
|
||||||
|
UserId: editor.UserId,
|
||||||
|
Permission: m.PERMISSION_ADMIN,
|
||||||
|
}}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &editor)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given an editor and a team in another org", func() {
|
||||||
|
testTeamOtherOrg := m.Team{
|
||||||
|
Id: 1,
|
||||||
|
OrgId: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("Shouldn't be able to update the team", func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error {
|
||||||
|
cmd.Result = []*m.TeamMemberDTO{{
|
||||||
|
OrgId: testTeamOtherOrg.OrgId,
|
||||||
|
TeamId: testTeamOtherOrg.Id,
|
||||||
|
UserId: editor.UserId,
|
||||||
|
Permission: m.PERMISSION_ADMIN,
|
||||||
|
}}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := CanAdmin(bus.GetBus(), testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor)
|
||||||
|
So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given an org admin and a team", func() {
|
||||||
|
Convey("Should be able to update the team", func() {
|
||||||
|
err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &admin)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -239,14 +239,13 @@ type Cfg struct {
|
|||||||
LoginMaxLifetimeDays int
|
LoginMaxLifetimeDays int
|
||||||
TokenRotationIntervalMinutes int
|
TokenRotationIntervalMinutes int
|
||||||
|
|
||||||
// User
|
|
||||||
EditorsCanOwn bool
|
|
||||||
|
|
||||||
// Dataproxy
|
// Dataproxy
|
||||||
SendUserHeader bool
|
SendUserHeader bool
|
||||||
|
|
||||||
// DistributedCache
|
// DistributedCache
|
||||||
RemoteCacheOptions *RemoteCacheOptions
|
RemoteCacheOptions *RemoteCacheOptions
|
||||||
|
|
||||||
|
EditorsCanAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
@ -670,7 +669,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
ExternalUserMngLinkName = users.Key("external_manage_link_name").String()
|
ExternalUserMngLinkName = users.Key("external_manage_link_name").String()
|
||||||
ExternalUserMngInfo = users.Key("external_manage_info").String()
|
ExternalUserMngInfo = users.Key("external_manage_info").String()
|
||||||
ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false)
|
ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false)
|
||||||
cfg.EditorsCanOwn = users.Key("editors_can_own").MustBool(false)
|
cfg.EditorsCanAdmin = users.Key("editors_can_admin").MustBool(false)
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
auth := iniFile.Section("auth")
|
auth := iniFile.Section("auth")
|
||||||
|
13
public/app/core/components/WithFeatureToggle.tsx
Normal file
13
public/app/core/components/WithFeatureToggle.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
featureToggle: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithFeatureToggle: FunctionComponent<Props> = ({ featureToggle, children }) => {
|
||||||
|
if (featureToggle === true) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => {
|
|||||||
{
|
{
|
||||||
link: {},
|
link: {},
|
||||||
user: {
|
user: {
|
||||||
|
id: 1,
|
||||||
isGrafanaAdmin: false,
|
isGrafanaAdmin: false,
|
||||||
isSignedIn: false,
|
isSignedIn: false,
|
||||||
orgCount: 2,
|
orgCount: 2,
|
||||||
|
@ -37,7 +37,7 @@ export class Settings {
|
|||||||
passwordHint: any;
|
passwordHint: any;
|
||||||
loginError: any;
|
loginError: any;
|
||||||
viewersCanEdit: boolean;
|
viewersCanEdit: boolean;
|
||||||
editorsCanOwn: boolean;
|
editorsCanAdmin: boolean;
|
||||||
disableSanitizeHtml: boolean;
|
disableSanitizeHtml: boolean;
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ export class Settings {
|
|||||||
isEnterprise: false,
|
isEnterprise: false,
|
||||||
},
|
},
|
||||||
viewersCanEdit: false,
|
viewersCanEdit: false,
|
||||||
editorsCanOwn: false,
|
editorsCanAdmin: false,
|
||||||
disableSanitizeHtml: false,
|
disableSanitizeHtml: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import _ from 'lodash';
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
|
id: number;
|
||||||
isGrafanaAdmin: any;
|
isGrafanaAdmin: any;
|
||||||
isSignedIn: any;
|
isSignedIn: any;
|
||||||
orgRole: any;
|
orgRole: any;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { Props, TeamList } from './TeamList';
|
import { Props, TeamList } from './TeamList';
|
||||||
import { NavModel, Team } from '../../types';
|
import { NavModel, Team, OrgRole } from '../../types';
|
||||||
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
|
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
|
||||||
|
import { User } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
@ -21,6 +22,11 @@ const setup = (propOverrides?: object) => {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
teamsCount: 0,
|
teamsCount: 0,
|
||||||
hasFetched: false,
|
hasFetched: false,
|
||||||
|
editorsCanAdmin: false,
|
||||||
|
signedInUser: {
|
||||||
|
id: 1,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
@ -49,6 +55,42 @@ describe('Render', () => {
|
|||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||||
|
describe('and signedin user is not viewer', () => {
|
||||||
|
it('should enable the new team button', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
teams: getMultipleMockTeams(1),
|
||||||
|
teamsCount: 1,
|
||||||
|
hasFetched: true,
|
||||||
|
editorsCanAdmin: true,
|
||||||
|
signedInUser: {
|
||||||
|
id: 1,
|
||||||
|
orgRole: OrgRole.Editor,
|
||||||
|
} as User,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and signedin user is a viewer', () => {
|
||||||
|
it('should disable the new team button', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
teams: getMultipleMockTeams(1),
|
||||||
|
teamsCount: 1,
|
||||||
|
hasFetched: true,
|
||||||
|
editorsCanAdmin: true,
|
||||||
|
signedInUser: {
|
||||||
|
id: 1,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Life cycle', () => {
|
describe('Life cycle', () => {
|
||||||
|
@ -4,11 +4,13 @@ import { hot } from 'react-hot-loader';
|
|||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { DeleteButton } from '@grafana/ui';
|
import { DeleteButton } from '@grafana/ui';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
import { NavModel, Team } from 'app/types';
|
import { NavModel, Team, OrgRole } from 'app/types';
|
||||||
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
|
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
|
||||||
import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
|
import { getSearchQuery, getTeams, getTeamsCount, isPermissionTeamAdmin } from './state/selectors';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
import { contextSrv, User } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
@ -19,6 +21,8 @@ export interface Props {
|
|||||||
loadTeams: typeof loadTeams;
|
loadTeams: typeof loadTeams;
|
||||||
deleteTeam: typeof deleteTeam;
|
deleteTeam: typeof deleteTeam;
|
||||||
setSearchQuery: typeof setSearchQuery;
|
setSearchQuery: typeof setSearchQuery;
|
||||||
|
editorsCanAdmin?: boolean;
|
||||||
|
signedInUser?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TeamList extends PureComponent<Props, any> {
|
export class TeamList extends PureComponent<Props, any> {
|
||||||
@ -39,7 +43,10 @@ export class TeamList extends PureComponent<Props, any> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderTeam(team: Team) {
|
renderTeam(team: Team) {
|
||||||
|
const { editorsCanAdmin, signedInUser } = this.props;
|
||||||
|
const permission = team.permission;
|
||||||
const teamUrl = `org/teams/edit/${team.id}`;
|
const teamUrl = `org/teams/edit/${team.id}`;
|
||||||
|
const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={team.id}>
|
<tr key={team.id}>
|
||||||
@ -58,7 +65,7 @@ export class TeamList extends PureComponent<Props, any> {
|
|||||||
<a href={teamUrl}>{team.memberCount}</a>
|
<a href={teamUrl}>{team.memberCount}</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right">
|
<td className="text-right">
|
||||||
<DeleteButton onConfirm={() => this.deleteTeam(team)} />
|
<DeleteButton onConfirm={() => this.deleteTeam(team)} disabled={!canDelete} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@ -84,7 +91,10 @@ export class TeamList extends PureComponent<Props, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTeamList() {
|
renderTeamList() {
|
||||||
const { teams, searchQuery } = this.props;
|
const { teams, searchQuery, editorsCanAdmin, signedInUser } = this.props;
|
||||||
|
const isCanAdminAndViewer = editorsCanAdmin && signedInUser.orgRole === OrgRole.Viewer;
|
||||||
|
const disabledClass = isCanAdminAndViewer ? ' disabled' : '';
|
||||||
|
const newTeamHref = isCanAdminAndViewer ? '#' : 'org/teams/new';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -101,7 +111,7 @@ export class TeamList extends PureComponent<Props, any> {
|
|||||||
|
|
||||||
<div className="page-action-bar__spacer" />
|
<div className="page-action-bar__spacer" />
|
||||||
|
|
||||||
<a className="btn btn-primary" href="org/teams/new">
|
<a className={`btn btn-primary${disabledClass}`} href={newTeamHref}>
|
||||||
New team
|
New team
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -152,6 +162,8 @@ function mapStateToProps(state) {
|
|||||||
searchQuery: getSearchQuery(state.teams),
|
searchQuery: getSearchQuery(state.teams),
|
||||||
teamsCount: getTeamsCount(state.teams),
|
teamsCount: getTeamsCount(state.teams),
|
||||||
hasFetched: state.teams.hasFetched,
|
hasFetched: state.teams.hasFetched,
|
||||||
|
editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
|
||||||
|
signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
90
public/app/features/teams/TeamMemberRow.test.tsx
Normal file
90
public/app/features/teams/TeamMemberRow.test.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { TeamMember, TeamPermissionLevel } from '../../types';
|
||||||
|
import { getMockTeamMember } from './__mocks__/teamMocks';
|
||||||
|
import { TeamMemberRow, Props } from './TeamMemberRow';
|
||||||
|
import { SelectOptionItem } from '@grafana/ui';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
member: getMockTeamMember(),
|
||||||
|
syncEnabled: false,
|
||||||
|
editorsCanAdmin: false,
|
||||||
|
signedInUserIsTeamAdmin: false,
|
||||||
|
updateTeamMember: jest.fn(),
|
||||||
|
removeTeamMember: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<TeamMemberRow {...props} />);
|
||||||
|
const instance = wrapper.instance() as TeamMemberRow;
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render team members when sync enabled', () => {
|
||||||
|
const member = getMockTeamMember();
|
||||||
|
member.labels = ['LDAP'];
|
||||||
|
const { wrapper } = setup({ member, syncEnabled: true });
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||||
|
it('should render permissions select if user is team admin', () => {
|
||||||
|
const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: true });
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render span and disable buttons if user is team member', () => {
|
||||||
|
const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: false });
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when feature toggle editorsCanAdmin is turned off', () => {
|
||||||
|
it('should not render permissions', () => {
|
||||||
|
const { wrapper } = setup({ editorsCanAdmin: false, signedInUserIsTeamAdmin: true });
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Functions', () => {
|
||||||
|
describe('on remove member', () => {
|
||||||
|
const member = getMockTeamMember();
|
||||||
|
const { instance } = setup({ member });
|
||||||
|
|
||||||
|
instance.onRemoveMember(member);
|
||||||
|
|
||||||
|
expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on update permision for user in team', () => {
|
||||||
|
const member: TeamMember = {
|
||||||
|
userId: 3,
|
||||||
|
teamId: 2,
|
||||||
|
avatarUrl: '',
|
||||||
|
email: 'user@user.org',
|
||||||
|
labels: [],
|
||||||
|
login: 'member',
|
||||||
|
permission: TeamPermissionLevel.Member,
|
||||||
|
};
|
||||||
|
const { instance } = setup({ member });
|
||||||
|
const permission = TeamPermissionLevel.Admin;
|
||||||
|
const item: SelectOptionItem = { value: permission };
|
||||||
|
const expectedTeamMemeber = { ...member, permission };
|
||||||
|
|
||||||
|
instance.onPermissionChange(item, member);
|
||||||
|
|
||||||
|
expect(instance.props.updateTeamMember).toHaveBeenCalledWith(expectedTeamMemeber);
|
||||||
|
});
|
||||||
|
});
|
106
public/app/features/teams/TeamMemberRow.tsx
Normal file
106
public/app/features/teams/TeamMemberRow.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { DeleteButton, Select, SelectOptionItem } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { TeamMember, teamsPermissionLevels } from 'app/types';
|
||||||
|
import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
|
||||||
|
import { updateTeamMember, removeTeamMember } from './state/actions';
|
||||||
|
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
member: TeamMember;
|
||||||
|
syncEnabled: boolean;
|
||||||
|
editorsCanAdmin: boolean;
|
||||||
|
signedInUserIsTeamAdmin: boolean;
|
||||||
|
removeTeamMember?: typeof removeTeamMember;
|
||||||
|
updateTeamMember?: typeof updateTeamMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamMemberRow extends PureComponent<Props> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.renderLabels = this.renderLabels.bind(this);
|
||||||
|
this.renderPermissions = this.renderPermissions.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveMember(member: TeamMember) {
|
||||||
|
this.props.removeTeamMember(member.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPermissionChange = (item: SelectOptionItem, member: TeamMember) => {
|
||||||
|
const permission = item.value;
|
||||||
|
const updatedTeamMember = { ...member, permission };
|
||||||
|
|
||||||
|
this.props.updateTeamMember(updatedTeamMember);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderPermissions(member: TeamMember) {
|
||||||
|
const { editorsCanAdmin, signedInUserIsTeamAdmin } = this.props;
|
||||||
|
const value = teamsPermissionLevels.find(dp => dp.value === member.permission);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WithFeatureToggle featureToggle={editorsCanAdmin}>
|
||||||
|
<td className="width-5 team-permissions">
|
||||||
|
<div className="gf-form">
|
||||||
|
{signedInUserIsTeamAdmin && (
|
||||||
|
<Select
|
||||||
|
isSearchable={false}
|
||||||
|
options={teamsPermissionLevels}
|
||||||
|
onChange={item => this.onPermissionChange(item, member)}
|
||||||
|
className="gf-form-select-box__control--menu-right"
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!signedInUserIsTeamAdmin && <span>{value.label}</span>}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</WithFeatureToggle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLabels(labels: string[]) {
|
||||||
|
if (!labels) {
|
||||||
|
return <td />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td>
|
||||||
|
{labels.map(label => (
|
||||||
|
<TagBadge key={label} label={label} removeIcon={false} count={0} onClick={() => {}} />
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { member, syncEnabled, signedInUserIsTeamAdmin } = this.props;
|
||||||
|
return (
|
||||||
|
<tr key={member.userId}>
|
||||||
|
<td className="width-4 text-center">
|
||||||
|
<img className="filter-table__avatar" src={member.avatarUrl} />
|
||||||
|
</td>
|
||||||
|
<td>{member.login}</td>
|
||||||
|
<td>{member.email}</td>
|
||||||
|
{this.renderPermissions(member)}
|
||||||
|
{syncEnabled && this.renderLabels(member.labels)}
|
||||||
|
<td className="text-right">
|
||||||
|
<DeleteButton onConfirm={() => this.onRemoveMember(member)} disabled={!signedInUserIsTeamAdmin} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
removeTeamMember,
|
||||||
|
updateTeamMember,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TeamMemberRow);
|
@ -1,18 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { TeamMembers, Props, State } from './TeamMembers';
|
import { TeamMembers, Props, State } from './TeamMembers';
|
||||||
import { TeamMember } from '../../types';
|
import { TeamMember, OrgRole } from '../../types';
|
||||||
import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks';
|
import { getMockTeamMembers } from './__mocks__/teamMocks';
|
||||||
|
import { User } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
|
const signedInUserId = 1;
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
members: [] as TeamMember[],
|
members: [] as TeamMember[],
|
||||||
searchMemberQuery: '',
|
searchMemberQuery: '',
|
||||||
setSearchMemberQuery: jest.fn(),
|
setSearchMemberQuery: jest.fn(),
|
||||||
loadTeamMembers: jest.fn(),
|
|
||||||
addTeamMember: jest.fn(),
|
addTeamMember: jest.fn(),
|
||||||
removeTeamMember: jest.fn(),
|
|
||||||
syncEnabled: false,
|
syncEnabled: false,
|
||||||
|
editorsCanAdmin: false,
|
||||||
|
signedInUser: {
|
||||||
|
id: signedInUserId,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
@ -28,24 +35,13 @@ const setup = (propOverrides?: object) => {
|
|||||||
|
|
||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
it('should render component', () => {
|
it('should render component', () => {
|
||||||
const { wrapper } = setup();
|
const { wrapper } = setup({});
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render team members', () => {
|
it('should render team members', () => {
|
||||||
const { wrapper } = setup({
|
const { wrapper } = setup({ members: getMockTeamMembers(5, 5) });
|
||||||
members: getMockTeamMembers(5),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render team members when sync enabled', () => {
|
|
||||||
const { wrapper } = setup({
|
|
||||||
members: getMockTeamMembers(5),
|
|
||||||
syncEnabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@ -54,7 +50,7 @@ describe('Render', () => {
|
|||||||
describe('Functions', () => {
|
describe('Functions', () => {
|
||||||
describe('on search member query change', () => {
|
describe('on search member query change', () => {
|
||||||
it('it should call setSearchMemberQuery', () => {
|
it('it should call setSearchMemberQuery', () => {
|
||||||
const { instance } = setup();
|
const { instance } = setup({});
|
||||||
|
|
||||||
instance.onSearchQueryChange('member');
|
instance.onSearchQueryChange('member');
|
||||||
|
|
||||||
@ -62,17 +58,8 @@ describe('Functions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on remove member', () => {
|
|
||||||
const { instance } = setup();
|
|
||||||
const mockTeamMember = getMockTeamMember();
|
|
||||||
|
|
||||||
instance.onRemoveMember(mockTeamMember);
|
|
||||||
|
|
||||||
expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('on add user to team', () => {
|
describe('on add user to team', () => {
|
||||||
const { wrapper, instance } = setup();
|
const { wrapper, instance } = setup({});
|
||||||
const state = wrapper.state() as State;
|
const state = wrapper.state() as State;
|
||||||
|
|
||||||
state.newTeamMember = {
|
state.newTeamMember = {
|
||||||
|
@ -2,21 +2,24 @@ import React, { PureComponent } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||||
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
||||||
import { DeleteButton } from '@grafana/ui';
|
|
||||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||||
import { TeamMember, User } from 'app/types';
|
import { TeamMember, User } from 'app/types';
|
||||||
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
|
import { addTeamMember, setSearchMemberQuery } from './state/actions';
|
||||||
import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
|
import { getSearchMemberQuery, isSignedInUserTeamAdmin } from './state/selectors';
|
||||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||||
|
import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
import { contextSrv, User as SignedInUser } from 'app/core/services/context_srv';
|
||||||
|
import TeamMemberRow from './TeamMemberRow';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
members: TeamMember[];
|
members: TeamMember[];
|
||||||
searchMemberQuery: string;
|
searchMemberQuery: string;
|
||||||
loadTeamMembers: typeof loadTeamMembers;
|
|
||||||
addTeamMember: typeof addTeamMember;
|
addTeamMember: typeof addTeamMember;
|
||||||
removeTeamMember: typeof removeTeamMember;
|
|
||||||
setSearchMemberQuery: typeof setSearchMemberQuery;
|
setSearchMemberQuery: typeof setSearchMemberQuery;
|
||||||
syncEnabled: boolean;
|
syncEnabled: boolean;
|
||||||
|
editorsCanAdmin?: boolean;
|
||||||
|
signedInUser?: SignedInUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
@ -30,18 +33,10 @@ export class TeamMembers extends PureComponent<Props, State> {
|
|||||||
this.state = { isAdding: false, newTeamMember: null };
|
this.state = { isAdding: false, newTeamMember: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadTeamMembers();
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchQueryChange = (value: string) => {
|
onSearchQueryChange = (value: string) => {
|
||||||
this.props.setSearchMemberQuery(value);
|
this.props.setSearchMemberQuery(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
onRemoveMember(member: TeamMember) {
|
|
||||||
this.props.removeTeamMember(member.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleAdding = () => {
|
onToggleAdding = () => {
|
||||||
this.setState({ isAdding: !this.state.isAdding });
|
this.setState({ isAdding: !this.state.isAdding });
|
||||||
};
|
};
|
||||||
@ -69,25 +64,11 @@ export class TeamMembers extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMember(member: TeamMember, syncEnabled: boolean) {
|
|
||||||
return (
|
|
||||||
<tr key={member.userId}>
|
|
||||||
<td className="width-4 text-center">
|
|
||||||
<img className="filter-table__avatar" src={member.avatarUrl} />
|
|
||||||
</td>
|
|
||||||
<td>{member.login}</td>
|
|
||||||
<td>{member.email}</td>
|
|
||||||
{syncEnabled && this.renderLabels(member.labels)}
|
|
||||||
<td className="text-right">
|
|
||||||
<DeleteButton onConfirm={() => this.onRemoveMember(member)} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isAdding } = this.state;
|
const { isAdding } = this.state;
|
||||||
const { searchMemberQuery, members, syncEnabled } = this.props;
|
const { searchMemberQuery, members, syncEnabled, editorsCanAdmin, signedInUser } = this.props;
|
||||||
|
const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-action-bar">
|
<div className="page-action-bar">
|
||||||
@ -103,7 +84,11 @@ export class TeamMembers extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
<div className="page-action-bar__spacer" />
|
<div className="page-action-bar__spacer" />
|
||||||
|
|
||||||
<button className="btn btn-primary pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
|
<button
|
||||||
|
className="btn btn-primary pull-right"
|
||||||
|
onClick={this.onToggleAdding}
|
||||||
|
disabled={isAdding || !isTeamAdmin}
|
||||||
|
>
|
||||||
Add member
|
Add member
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -132,11 +117,25 @@ export class TeamMembers extends PureComponent<Props, State> {
|
|||||||
<th />
|
<th />
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
|
<WithFeatureToggle featureToggle={editorsCanAdmin}>
|
||||||
|
<th>Permission</th>
|
||||||
|
</WithFeatureToggle>
|
||||||
{syncEnabled && <th />}
|
{syncEnabled && <th />}
|
||||||
<th style={{ width: '1%' }} />
|
<th style={{ width: '1%' }} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{members && members.map(member => this.renderMember(member, syncEnabled))}</tbody>
|
<tbody>
|
||||||
|
{members &&
|
||||||
|
members.map(member => (
|
||||||
|
<TeamMemberRow
|
||||||
|
key={member.userId}
|
||||||
|
member={member}
|
||||||
|
syncEnabled={syncEnabled}
|
||||||
|
editorsCanAdmin={editorsCanAdmin}
|
||||||
|
signedInUserIsTeamAdmin={isTeamAdmin}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -146,15 +145,14 @@ export class TeamMembers extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
members: getTeamMembers(state.team),
|
|
||||||
searchMemberQuery: getSearchMemberQuery(state.team),
|
searchMemberQuery: getSearchMemberQuery(state.team),
|
||||||
|
editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
|
||||||
|
signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
loadTeamMembers,
|
|
||||||
addTeamMember,
|
addTeamMember,
|
||||||
removeTeamMember,
|
|
||||||
setSearchMemberQuery,
|
setSearchMemberQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { TeamPages, Props } from './TeamPages';
|
import { TeamPages, Props } from './TeamPages';
|
||||||
import { NavModel, Team } from '../../types';
|
import { NavModel, Team, TeamMember, OrgRole } from '../../types';
|
||||||
import { getMockTeam } from './__mocks__/teamMocks';
|
import { getMockTeam } from './__mocks__/teamMocks';
|
||||||
|
import { User } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
jest.mock('app/core/config', () => ({
|
jest.mock('app/core/config', () => ({
|
||||||
buildInfo: { isEnterprise: true },
|
buildInfo: { isEnterprise: true },
|
||||||
@ -13,8 +14,16 @@ const setup = (propOverrides?: object) => {
|
|||||||
navModel: {} as NavModel,
|
navModel: {} as NavModel,
|
||||||
teamId: 1,
|
teamId: 1,
|
||||||
loadTeam: jest.fn(),
|
loadTeam: jest.fn(),
|
||||||
|
loadTeamMembers: jest.fn(),
|
||||||
pageName: 'members',
|
pageName: 'members',
|
||||||
team: {} as Team,
|
team: {} as Team,
|
||||||
|
members: [] as TeamMember[],
|
||||||
|
editorsCanAdmin: false,
|
||||||
|
signedInUser: {
|
||||||
|
id: 1,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
@ -65,4 +74,46 @@ describe('Render', () => {
|
|||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||||
|
it('should render settings page if user is team admin', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
team: getMockTeam(),
|
||||||
|
pageName: 'settings',
|
||||||
|
preferences: {
|
||||||
|
homeDashboardId: 1,
|
||||||
|
theme: 'Default',
|
||||||
|
timezone: 'Default',
|
||||||
|
},
|
||||||
|
editorsCanAdmin: true,
|
||||||
|
signedInUser: {
|
||||||
|
id: 1,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
orgRole: OrgRole.Admin,
|
||||||
|
} as User,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render settings page if user is team member', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
team: getMockTeam(),
|
||||||
|
pageName: 'settings',
|
||||||
|
preferences: {
|
||||||
|
homeDashboardId: 1,
|
||||||
|
theme: 'Default',
|
||||||
|
timezone: 'Default',
|
||||||
|
},
|
||||||
|
editorsCanAdmin: true,
|
||||||
|
signedInUser: {
|
||||||
|
id: 1,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,19 +7,24 @@ import Page from 'app/core/components/Page/Page';
|
|||||||
import TeamMembers from './TeamMembers';
|
import TeamMembers from './TeamMembers';
|
||||||
import TeamSettings from './TeamSettings';
|
import TeamSettings from './TeamSettings';
|
||||||
import TeamGroupSync from './TeamGroupSync';
|
import TeamGroupSync from './TeamGroupSync';
|
||||||
import { NavModel, Team } from 'app/types';
|
import { NavModel, Team, TeamMember } from 'app/types';
|
||||||
import { loadTeam } from './state/actions';
|
import { loadTeam, loadTeamMembers } from './state/actions';
|
||||||
import { getTeam } from './state/selectors';
|
import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors';
|
||||||
import { getTeamLoadingNav } from './state/navModel';
|
import { getTeamLoadingNav } from './state/navModel';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
|
import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
|
||||||
|
import { contextSrv, User } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
team: Team;
|
team: Team;
|
||||||
loadTeam: typeof loadTeam;
|
loadTeam: typeof loadTeam;
|
||||||
|
loadTeamMembers: typeof loadTeamMembers;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
pageName: string;
|
pageName: string;
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
|
members?: TeamMember[];
|
||||||
|
editorsCanAdmin?: boolean;
|
||||||
|
signedInUser?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -51,6 +56,7 @@ export class TeamPages extends PureComponent<Props, State> {
|
|||||||
const { loadTeam, teamId } = this.props;
|
const { loadTeam, teamId } = this.props;
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
const team = await loadTeam(teamId);
|
const team = await loadTeam(teamId);
|
||||||
|
await this.props.loadTeamMembers();
|
||||||
this.setState({ isLoading: false });
|
this.setState({ isLoading: false });
|
||||||
return team;
|
return team;
|
||||||
}
|
}
|
||||||
@ -61,30 +67,56 @@ export class TeamPages extends PureComponent<Props, State> {
|
|||||||
return _.includes(pages, currentPage) ? currentPage : pages[0];
|
return _.includes(pages, currentPage) ? currentPage : pages[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPage() {
|
textsAreEqual = (text1: string, text2: string) => {
|
||||||
|
if (!text1 && !text2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text1 || !text2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text1.toLocaleLowerCase() === text2.toLocaleLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => {
|
||||||
|
if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) {
|
||||||
|
navModel.main.children
|
||||||
|
.filter(navItem => !this.textsAreEqual(navItem.text, PageTypes.Members))
|
||||||
|
.map(navItem => {
|
||||||
|
navItem.hideFromTabs = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return navModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderPage(isSignedInUserTeamAdmin: boolean) {
|
||||||
const { isSyncEnabled } = this.state;
|
const { isSyncEnabled } = this.state;
|
||||||
|
const { members } = this.props;
|
||||||
const currentPage = this.getCurrentPage();
|
const currentPage = this.getCurrentPage();
|
||||||
|
|
||||||
switch (currentPage) {
|
switch (currentPage) {
|
||||||
case PageTypes.Members:
|
case PageTypes.Members:
|
||||||
return <TeamMembers syncEnabled={isSyncEnabled} />;
|
return <TeamMembers syncEnabled={isSyncEnabled} members={members} />;
|
||||||
|
|
||||||
case PageTypes.Settings:
|
case PageTypes.Settings:
|
||||||
return <TeamSettings />;
|
return isSignedInUserTeamAdmin && <TeamSettings />;
|
||||||
case PageTypes.GroupSync:
|
case PageTypes.GroupSync:
|
||||||
return isSyncEnabled && <TeamGroupSync />;
|
return isSignedInUserTeamAdmin && isSyncEnabled && <TeamGroupSync />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { team, navModel } = this.props;
|
const { team, navModel, members, editorsCanAdmin, signedInUser } = this.props;
|
||||||
|
const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel}>
|
<Page navModel={this.hideTabsFromNonTeamAdmin(navModel, isTeamAdmin)}>
|
||||||
<Page.Contents isLoading={this.state.isLoading}>
|
<Page.Contents isLoading={this.state.isLoading}>
|
||||||
{team && Object.keys(team).length !== 0 && this.renderPage()}
|
{team && Object.keys(team).length !== 0 && this.renderPage(isTeamAdmin)}
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
@ -95,17 +127,24 @@ function mapStateToProps(state) {
|
|||||||
const teamId = getRouteParamsId(state.location);
|
const teamId = getRouteParamsId(state.location);
|
||||||
const pageName = getRouteParamsPage(state.location) || 'members';
|
const pageName = getRouteParamsPage(state.location) || 'members';
|
||||||
const teamLoadingNav = getTeamLoadingNav(pageName);
|
const teamLoadingNav = getTeamLoadingNav(pageName);
|
||||||
|
const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav);
|
||||||
|
const team = getTeam(state.team, teamId);
|
||||||
|
const members = getTeamMembers(state.team);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav),
|
navModel,
|
||||||
teamId: teamId,
|
teamId: teamId,
|
||||||
pageName: pageName,
|
pageName: pageName,
|
||||||
team: getTeam(state.team, teamId),
|
team,
|
||||||
|
members,
|
||||||
|
editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
|
||||||
|
signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
loadTeam,
|
loadTeam,
|
||||||
|
loadTeamMembers,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(
|
export default hot(module)(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Team, TeamGroup, TeamMember } from 'app/types';
|
import { Team, TeamGroup, TeamMember, TeamPermissionLevel } from 'app/types';
|
||||||
|
|
||||||
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
|
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
|
||||||
const teams: Team[] = [];
|
const teams: Team[] = [];
|
||||||
@ -9,6 +9,7 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
|
|||||||
avatarUrl: 'some/url/',
|
avatarUrl: 'some/url/',
|
||||||
email: `test-${i}@test.com`,
|
email: `test-${i}@test.com`,
|
||||||
memberCount: i,
|
memberCount: i,
|
||||||
|
permission: TeamPermissionLevel.Member,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,10 +23,11 @@ export const getMockTeam = (): Team => {
|
|||||||
avatarUrl: 'some/url/',
|
avatarUrl: 'some/url/',
|
||||||
email: 'test@test.com',
|
email: 'test@test.com',
|
||||||
memberCount: 1,
|
memberCount: 1,
|
||||||
|
permission: TeamPermissionLevel.Member,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMockTeamMembers = (amount: number): TeamMember[] => {
|
export const getMockTeamMembers = (amount: number, teamAdminId: number): TeamMember[] => {
|
||||||
const teamMembers: TeamMember[] = [];
|
const teamMembers: TeamMember[] = [];
|
||||||
|
|
||||||
for (let i = 1; i <= amount; i++) {
|
for (let i = 1; i <= amount; i++) {
|
||||||
@ -36,6 +38,7 @@ export const getMockTeamMembers = (amount: number): TeamMember[] => {
|
|||||||
email: 'test@test.com',
|
email: 'test@test.com',
|
||||||
login: `testUser-${i}`,
|
login: `testUser-${i}`,
|
||||||
labels: ['label 1', 'label 2'],
|
labels: ['label 1', 'label 2'],
|
||||||
|
permission: i === teamAdminId ? TeamPermissionLevel.Admin : TeamPermissionLevel.Member,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +53,7 @@ export const getMockTeamMember = (): TeamMember => {
|
|||||||
email: 'test@test.com',
|
email: 'test@test.com',
|
||||||
login: 'testUser',
|
login: 'testUser',
|
||||||
labels: [],
|
labels: [],
|
||||||
|
permission: TeamPermissionLevel.Member,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,6 +133,7 @@ exports[`Render should render teams table 1`] = `
|
|||||||
className="text-right"
|
className="text-right"
|
||||||
>
|
>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
|
disabled={false}
|
||||||
onConfirm={[Function]}
|
onConfirm={[Function]}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@ -183,6 +184,7 @@ exports[`Render should render teams table 1`] = `
|
|||||||
className="text-right"
|
className="text-right"
|
||||||
>
|
>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
|
disabled={false}
|
||||||
onConfirm={[Function]}
|
onConfirm={[Function]}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@ -233,6 +235,7 @@ exports[`Render should render teams table 1`] = `
|
|||||||
className="text-right"
|
className="text-right"
|
||||||
>
|
>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
|
disabled={false}
|
||||||
onConfirm={[Function]}
|
onConfirm={[Function]}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@ -283,6 +286,7 @@ exports[`Render should render teams table 1`] = `
|
|||||||
className="text-right"
|
className="text-right"
|
||||||
>
|
>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
|
disabled={false}
|
||||||
onConfirm={[Function]}
|
onConfirm={[Function]}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@ -333,6 +337,259 @@ exports[`Render should render teams table 1`] = `
|
|||||||
className="text-right"
|
className="text-right"
|
||||||
>
|
>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
|
disabled={false}
|
||||||
|
onConfirm={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</PageContents>
|
||||||
|
</Page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render when feature toggle editorsCanAdmin is turned on and signedin user is a viewer should disable the new team button 1`] = `
|
||||||
|
<Page
|
||||||
|
navModel={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"text": "Configuration",
|
||||||
|
},
|
||||||
|
"node": Object {
|
||||||
|
"text": "Team List",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PageContents
|
||||||
|
isLoading={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="page-action-bar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<ForwardRef
|
||||||
|
inputClassName="gf-form-input"
|
||||||
|
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Search teams"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar__spacer"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
className="btn btn-primary disabled"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
New team
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="admin-list-table"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
className="filter-table filter-table--hover form-inline"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Members
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "1%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
key="1"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/1"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/1"
|
||||||
|
>
|
||||||
|
test-1
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/1"
|
||||||
|
>
|
||||||
|
test-1@test.com
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/1"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
disabled={true}
|
||||||
|
onConfirm={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</PageContents>
|
||||||
|
</Page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render when feature toggle editorsCanAdmin is turned on and signedin user is not viewer should enable the new team button 1`] = `
|
||||||
|
<Page
|
||||||
|
navModel={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"text": "Configuration",
|
||||||
|
},
|
||||||
|
"node": Object {
|
||||||
|
"text": "Team List",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PageContents
|
||||||
|
isLoading={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="page-action-bar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<ForwardRef
|
||||||
|
inputClassName="gf-form-input"
|
||||||
|
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Search teams"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar__spacer"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
className="btn btn-primary"
|
||||||
|
href="org/teams/new"
|
||||||
|
>
|
||||||
|
New team
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="admin-list-table"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
className="filter-table filter-table--hover form-inline"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Members
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "1%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
key="1"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/1"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/1"
|
||||||
|
>
|
||||||
|
test-1
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/1"
|
||||||
|
>
|
||||||
|
test-1@test.com
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/1"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
disabled={true}
|
||||||
onConfirm={[Function]}
|
onConfirm={[Function]}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
@ -0,0 +1,250 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render team members when sync enabled 1`] = `
|
||||||
|
<tr
|
||||||
|
key="1"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
testUser
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
test@test.com
|
||||||
|
</td>
|
||||||
|
<Component
|
||||||
|
featureToggle={false}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-5 team-permissions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Member
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</Component>
|
||||||
|
<td>
|
||||||
|
<TagBadge
|
||||||
|
count={0}
|
||||||
|
key="LDAP"
|
||||||
|
label="LDAP"
|
||||||
|
onClick={[Function]}
|
||||||
|
removeIcon={false}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
disabled={true}
|
||||||
|
onConfirm={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render when feature toggle editorsCanAdmin is turned off should not render permissions 1`] = `
|
||||||
|
<tr
|
||||||
|
key="1"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
testUser
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
test@test.com
|
||||||
|
</td>
|
||||||
|
<Component
|
||||||
|
featureToggle={false}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-5 team-permissions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
autoFocus={false}
|
||||||
|
backspaceRemovesValue={true}
|
||||||
|
className="gf-form-select-box__control--menu-right"
|
||||||
|
isClearable={false}
|
||||||
|
isDisabled={false}
|
||||||
|
isLoading={false}
|
||||||
|
isMulti={false}
|
||||||
|
isSearchable={false}
|
||||||
|
maxMenuHeight={300}
|
||||||
|
onChange={[Function]}
|
||||||
|
openMenuOnFocus={false}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"description": "Is team member",
|
||||||
|
"label": "Member",
|
||||||
|
"value": 0,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"description": "Can add/remove permissions, members and delete team.",
|
||||||
|
"label": "Admin",
|
||||||
|
"value": 4,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
Object {
|
||||||
|
"description": "Is team member",
|
||||||
|
"label": "Member",
|
||||||
|
"value": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width={null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</Component>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
disabled={false}
|
||||||
|
onConfirm={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render when feature toggle editorsCanAdmin is turned on should render permissions select if user is team admin 1`] = `
|
||||||
|
<tr
|
||||||
|
key="1"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
testUser
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
test@test.com
|
||||||
|
</td>
|
||||||
|
<Component
|
||||||
|
featureToggle={true}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-5 team-permissions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
autoFocus={false}
|
||||||
|
backspaceRemovesValue={true}
|
||||||
|
className="gf-form-select-box__control--menu-right"
|
||||||
|
isClearable={false}
|
||||||
|
isDisabled={false}
|
||||||
|
isLoading={false}
|
||||||
|
isMulti={false}
|
||||||
|
isSearchable={false}
|
||||||
|
maxMenuHeight={300}
|
||||||
|
onChange={[Function]}
|
||||||
|
openMenuOnFocus={false}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"description": "Is team member",
|
||||||
|
"label": "Member",
|
||||||
|
"value": 0,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"description": "Can add/remove permissions, members and delete team.",
|
||||||
|
"label": "Admin",
|
||||||
|
"value": 4,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
Object {
|
||||||
|
"description": "Is team member",
|
||||||
|
"label": "Member",
|
||||||
|
"value": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width={null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</Component>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
disabled={false}
|
||||||
|
onConfirm={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render when feature toggle editorsCanAdmin is turned on should render span and disable buttons if user is team member 1`] = `
|
||||||
|
<tr
|
||||||
|
key="1"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
testUser
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
test@test.com
|
||||||
|
</td>
|
||||||
|
<Component
|
||||||
|
featureToggle={true}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-5 team-permissions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Member
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</Component>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
disabled={true}
|
||||||
|
onConfirm={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
@ -69,6 +69,13 @@ exports[`Render should render component 1`] = `
|
|||||||
<th>
|
<th>
|
||||||
Email
|
Email
|
||||||
</th>
|
</th>
|
||||||
|
<Component
|
||||||
|
featureToggle={false}
|
||||||
|
>
|
||||||
|
<th>
|
||||||
|
Permission
|
||||||
|
</th>
|
||||||
|
</Component>
|
||||||
<th
|
<th
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
@ -153,6 +160,13 @@ exports[`Render should render team members 1`] = `
|
|||||||
<th>
|
<th>
|
||||||
Email
|
Email
|
||||||
</th>
|
</th>
|
||||||
|
<Component
|
||||||
|
featureToggle={false}
|
||||||
|
>
|
||||||
|
<th>
|
||||||
|
Permission
|
||||||
|
</th>
|
||||||
|
</Component>
|
||||||
<th
|
<th
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
@ -163,422 +177,106 @@ exports[`Render should render team members 1`] = `
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<Connect(TeamMemberRow)
|
||||||
|
editorsCanAdmin={false}
|
||||||
key="1"
|
key="1"
|
||||||
>
|
member={
|
||||||
<td
|
Object {
|
||||||
className="width-4 text-center"
|
"avatarUrl": "some/url/",
|
||||||
>
|
"email": "test@test.com",
|
||||||
<img
|
"labels": Array [
|
||||||
className="filter-table__avatar"
|
"label 1",
|
||||||
src="some/url/"
|
"label 2",
|
||||||
/>
|
],
|
||||||
</td>
|
"login": "testUser-1",
|
||||||
<td>
|
"permission": 0,
|
||||||
testUser-1
|
"teamId": 1,
|
||||||
</td>
|
"userId": 1,
|
||||||
<td>
|
|
||||||
test@test.com
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
key="2"
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
className="width-4 text-center"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="filter-table__avatar"
|
|
||||||
src="some/url/"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
testUser-2
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
test@test.com
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
key="3"
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
className="width-4 text-center"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="filter-table__avatar"
|
|
||||||
src="some/url/"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
testUser-3
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
test@test.com
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
key="4"
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
className="width-4 text-center"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="filter-table__avatar"
|
|
||||||
src="some/url/"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
testUser-4
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
test@test.com
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
key="5"
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
className="width-4 text-center"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="filter-table__avatar"
|
|
||||||
src="some/url/"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
testUser-5
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
test@test.com
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Render should render team members when sync enabled 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="page-action-bar"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gf-form gf-form--grow"
|
|
||||||
>
|
|
||||||
<ForwardRef
|
|
||||||
inputClassName="gf-form-input"
|
|
||||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
|
||||||
onChange={[Function]}
|
|
||||||
placeholder="Search members"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="page-action-bar__spacer"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary pull-right"
|
|
||||||
disabled={false}
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
Add member
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Component
|
|
||||||
in={false}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="cta-form"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="cta-form__close btn btn-transparent"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className="fa fa-close"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<h5>
|
|
||||||
Add team member
|
|
||||||
</h5>
|
|
||||||
<div
|
|
||||||
className="gf-form-inline"
|
|
||||||
>
|
|
||||||
<UserPicker
|
|
||||||
className="min-width-30"
|
|
||||||
onSelected={[Function]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Component>
|
|
||||||
<div
|
|
||||||
className="admin-list-table"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
className="filter-table filter-table--hover form-inline"
|
|
||||||
>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th />
|
|
||||||
<th>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
Email
|
|
||||||
</th>
|
|
||||||
<th />
|
|
||||||
<th
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"width": "1%",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
</tr>
|
signedInUserIsTeamAdmin={true}
|
||||||
</thead>
|
syncEnabled={false}
|
||||||
<tbody>
|
/>
|
||||||
<tr
|
<Connect(TeamMemberRow)
|
||||||
key="1"
|
editorsCanAdmin={false}
|
||||||
>
|
|
||||||
<td
|
|
||||||
className="width-4 text-center"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="filter-table__avatar"
|
|
||||||
src="some/url/"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
testUser-1
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
test@test.com
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<TagBadge
|
|
||||||
count={0}
|
|
||||||
key="label 1"
|
|
||||||
label="label 1"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
<TagBadge
|
|
||||||
count={0}
|
|
||||||
key="label 2"
|
|
||||||
label="label 2"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
key="2"
|
key="2"
|
||||||
>
|
member={
|
||||||
<td
|
Object {
|
||||||
className="width-4 text-center"
|
"avatarUrl": "some/url/",
|
||||||
>
|
"email": "test@test.com",
|
||||||
<img
|
"labels": Array [
|
||||||
className="filter-table__avatar"
|
"label 1",
|
||||||
src="some/url/"
|
"label 2",
|
||||||
/>
|
],
|
||||||
</td>
|
"login": "testUser-2",
|
||||||
<td>
|
"permission": 0,
|
||||||
testUser-2
|
"teamId": 1,
|
||||||
</td>
|
"userId": 2,
|
||||||
<td>
|
}
|
||||||
test@test.com
|
}
|
||||||
</td>
|
signedInUserIsTeamAdmin={true}
|
||||||
<td>
|
syncEnabled={false}
|
||||||
<TagBadge
|
/>
|
||||||
count={0}
|
<Connect(TeamMemberRow)
|
||||||
key="label 1"
|
editorsCanAdmin={false}
|
||||||
label="label 1"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
<TagBadge
|
|
||||||
count={0}
|
|
||||||
key="label 2"
|
|
||||||
label="label 2"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
key="3"
|
key="3"
|
||||||
>
|
member={
|
||||||
<td
|
Object {
|
||||||
className="width-4 text-center"
|
"avatarUrl": "some/url/",
|
||||||
>
|
"email": "test@test.com",
|
||||||
<img
|
"labels": Array [
|
||||||
className="filter-table__avatar"
|
"label 1",
|
||||||
src="some/url/"
|
"label 2",
|
||||||
/>
|
],
|
||||||
</td>
|
"login": "testUser-3",
|
||||||
<td>
|
"permission": 0,
|
||||||
testUser-3
|
"teamId": 1,
|
||||||
</td>
|
"userId": 3,
|
||||||
<td>
|
}
|
||||||
test@test.com
|
}
|
||||||
</td>
|
signedInUserIsTeamAdmin={true}
|
||||||
<td>
|
syncEnabled={false}
|
||||||
<TagBadge
|
/>
|
||||||
count={0}
|
<Connect(TeamMemberRow)
|
||||||
key="label 1"
|
editorsCanAdmin={false}
|
||||||
label="label 1"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
<TagBadge
|
|
||||||
count={0}
|
|
||||||
key="label 2"
|
|
||||||
label="label 2"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
key="4"
|
key="4"
|
||||||
>
|
member={
|
||||||
<td
|
Object {
|
||||||
className="width-4 text-center"
|
"avatarUrl": "some/url/",
|
||||||
>
|
"email": "test@test.com",
|
||||||
<img
|
"labels": Array [
|
||||||
className="filter-table__avatar"
|
"label 1",
|
||||||
src="some/url/"
|
"label 2",
|
||||||
/>
|
],
|
||||||
</td>
|
"login": "testUser-4",
|
||||||
<td>
|
"permission": 0,
|
||||||
testUser-4
|
"teamId": 1,
|
||||||
</td>
|
"userId": 4,
|
||||||
<td>
|
}
|
||||||
test@test.com
|
}
|
||||||
</td>
|
signedInUserIsTeamAdmin={true}
|
||||||
<td>
|
syncEnabled={false}
|
||||||
<TagBadge
|
/>
|
||||||
count={0}
|
<Connect(TeamMemberRow)
|
||||||
key="label 1"
|
editorsCanAdmin={false}
|
||||||
label="label 1"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
<TagBadge
|
|
||||||
count={0}
|
|
||||||
key="label 2"
|
|
||||||
label="label 2"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
key="5"
|
key="5"
|
||||||
>
|
member={
|
||||||
<td
|
Object {
|
||||||
className="width-4 text-center"
|
"avatarUrl": "some/url/",
|
||||||
>
|
"email": "test@test.com",
|
||||||
<img
|
"labels": Array [
|
||||||
className="filter-table__avatar"
|
"label 1",
|
||||||
src="some/url/"
|
"label 2",
|
||||||
/>
|
],
|
||||||
</td>
|
"login": "testUser-5",
|
||||||
<td>
|
"permission": 4,
|
||||||
testUser-5
|
"teamId": 1,
|
||||||
</td>
|
"userId": 5,
|
||||||
<td>
|
}
|
||||||
test@test.com
|
}
|
||||||
</td>
|
signedInUserIsTeamAdmin={true}
|
||||||
<td>
|
syncEnabled={false}
|
||||||
<TagBadge
|
/>
|
||||||
count={0}
|
|
||||||
key="label 1"
|
|
||||||
label="label 1"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
<TagBadge
|
|
||||||
count={0}
|
|
||||||
key="label 2"
|
|
||||||
label="label 2"
|
|
||||||
onClick={[Function]}
|
|
||||||
removeIcon={false}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
<DeleteButton
|
|
||||||
onConfirm={[Function]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,6 +30,7 @@ exports[`Render should render member page if team not empty 1`] = `
|
|||||||
isLoading={true}
|
isLoading={true}
|
||||||
>
|
>
|
||||||
<Connect(TeamMembers)
|
<Connect(TeamMembers)
|
||||||
|
members={Array []}
|
||||||
syncEnabled={true}
|
syncEnabled={true}
|
||||||
/>
|
/>
|
||||||
</PageContents>
|
</PageContents>
|
||||||
@ -47,3 +48,25 @@ exports[`Render should render settings and preferences page 1`] = `
|
|||||||
</PageContents>
|
</PageContents>
|
||||||
</Page>
|
</Page>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Render when feature toggle editorsCanAdmin is turned on should not render settings page if user is team member 1`] = `
|
||||||
|
<Page
|
||||||
|
navModel={Object {}}
|
||||||
|
>
|
||||||
|
<PageContents
|
||||||
|
isLoading={true}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render when feature toggle editorsCanAdmin is turned on should render settings page if user is team admin 1`] = `
|
||||||
|
<Page
|
||||||
|
navModel={Object {}}
|
||||||
|
>
|
||||||
|
<PageContents
|
||||||
|
isLoading={true}
|
||||||
|
>
|
||||||
|
<Connect(TeamSettings) />
|
||||||
|
</PageContents>
|
||||||
|
</Page>
|
||||||
|
`;
|
||||||
|
@ -160,3 +160,12 @@ export function deleteTeam(id: number): ThunkResult<void> {
|
|||||||
dispatch(loadTeams());
|
dispatch(loadTeams());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateTeamMember(member: TeamMember): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().put(`/api/teams/${member.teamId}/members/${member.userId}`, {
|
||||||
|
permission: member.permission,
|
||||||
|
});
|
||||||
|
dispatch(loadTeamMembers());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Team, NavModelItem, NavModel } from 'app/types';
|
import { Team, NavModelItem, NavModel, TeamPermissionLevel } from 'app/types';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
export function buildNavModel(team: Team): NavModelItem {
|
export function buildNavModel(team: Team): NavModelItem {
|
||||||
@ -47,6 +47,7 @@ export function getTeamLoadingNav(pageName: string): NavModel {
|
|||||||
name: 'Loading',
|
name: 'Loading',
|
||||||
email: 'loading',
|
email: 'loading',
|
||||||
memberCount: 0,
|
memberCount: 0,
|
||||||
|
permission: TeamPermissionLevel.Member,
|
||||||
});
|
});
|
||||||
|
|
||||||
let node: NavModelItem;
|
let node: NavModelItem;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { getTeam, getTeamMembers, getTeams } from './selectors';
|
import { getTeam, getTeamMembers, getTeams, isSignedInUserTeamAdmin, Config } from './selectors';
|
||||||
import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks';
|
import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks';
|
||||||
import { Team, TeamGroup, TeamsState, TeamState } from '../../../types';
|
import { Team, TeamGroup, TeamsState, TeamState, OrgRole } from '../../../types';
|
||||||
|
import { User } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
describe('Teams selectors', () => {
|
describe('Teams selectors', () => {
|
||||||
describe('Get teams', () => {
|
describe('Get teams', () => {
|
||||||
@ -40,7 +41,7 @@ describe('Team selectors', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Get members', () => {
|
describe('Get members', () => {
|
||||||
const mockTeamMembers = getMockTeamMembers(5);
|
const mockTeamMembers = getMockTeamMembers(5, 5);
|
||||||
|
|
||||||
it('should return team members', () => {
|
it('should return team members', () => {
|
||||||
const mockState: TeamState = {
|
const mockState: TeamState = {
|
||||||
@ -55,3 +56,94 @@ describe('Team selectors', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const signedInUserId = 1;
|
||||||
|
|
||||||
|
const setup = (configOverrides?: Partial<Config>) => {
|
||||||
|
const defaultConfig: Config = {
|
||||||
|
editorsCanAdmin: false,
|
||||||
|
members: getMockTeamMembers(5, 5),
|
||||||
|
signedInUser: {
|
||||||
|
id: signedInUserId,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...defaultConfig, ...configOverrides };
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('isSignedInUserTeamAdmin', () => {
|
||||||
|
describe('when feature toggle editorsCanAdmin is turned off', () => {
|
||||||
|
it('should return true', () => {
|
||||||
|
const config = setup();
|
||||||
|
|
||||||
|
const result = isSignedInUserTeamAdmin(config);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||||
|
it('should return true if signed in user is grafanaAdmin', () => {
|
||||||
|
const config = setup({
|
||||||
|
editorsCanAdmin: true,
|
||||||
|
signedInUser: {
|
||||||
|
id: signedInUserId,
|
||||||
|
isGrafanaAdmin: true,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = isSignedInUserTeamAdmin(config);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if signed in user is org admin', () => {
|
||||||
|
const config = setup({
|
||||||
|
editorsCanAdmin: true,
|
||||||
|
signedInUser: {
|
||||||
|
id: signedInUserId,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
orgRole: OrgRole.Admin,
|
||||||
|
} as User,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = isSignedInUserTeamAdmin(config);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if signed in user is team admin', () => {
|
||||||
|
const config = setup({
|
||||||
|
members: getMockTeamMembers(5, signedInUserId),
|
||||||
|
editorsCanAdmin: true,
|
||||||
|
signedInUser: {
|
||||||
|
id: signedInUserId,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = isSignedInUserTeamAdmin(config);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if signed in user is not grafanaAdmin, org admin or team admin', () => {
|
||||||
|
const config = setup({
|
||||||
|
editorsCanAdmin: true,
|
||||||
|
signedInUser: {
|
||||||
|
id: signedInUserId,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
orgRole: OrgRole.Viewer,
|
||||||
|
} as User,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = isSignedInUserTeamAdmin(config);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Team, TeamsState, TeamState } from 'app/types';
|
import { Team, TeamsState, TeamState, TeamMember, OrgRole, TeamPermissionLevel } from 'app/types';
|
||||||
|
import { User } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
export const getSearchQuery = (state: TeamsState) => state.searchQuery;
|
export const getSearchQuery = (state: TeamsState) => state.searchQuery;
|
||||||
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
|
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
|
||||||
@ -28,3 +29,32 @@ export const getTeamMembers = (state: TeamState) => {
|
|||||||
return regex.test(member.login) || regex.test(member.email);
|
return regex.test(member.login) || regex.test(member.email);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
members: TeamMember[];
|
||||||
|
editorsCanAdmin: boolean;
|
||||||
|
signedInUser: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isSignedInUserTeamAdmin = (config: Config): boolean => {
|
||||||
|
const { members, signedInUser, editorsCanAdmin } = config;
|
||||||
|
const userInMembers = members.find(m => m.userId === signedInUser.id);
|
||||||
|
const permission = userInMembers ? userInMembers.permission : TeamPermissionLevel.Member;
|
||||||
|
|
||||||
|
return isPermissionTeamAdmin({ permission, signedInUser, editorsCanAdmin });
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PermissionConfig {
|
||||||
|
permission: TeamPermissionLevel;
|
||||||
|
editorsCanAdmin: boolean;
|
||||||
|
signedInUser: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isPermissionTeamAdmin = (config: PermissionConfig): boolean => {
|
||||||
|
const { permission, signedInUser, editorsCanAdmin } = config;
|
||||||
|
const isAdmin = signedInUser.isGrafanaAdmin || signedInUser.orgRole === OrgRole.Admin;
|
||||||
|
const userIsTeamAdmin = permission === TeamPermissionLevel.Admin;
|
||||||
|
const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin;
|
||||||
|
|
||||||
|
return isSignedInUserTeamAdmin || !editorsCanAdmin;
|
||||||
|
};
|
||||||
|
@ -195,7 +195,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
.when('/org/teams', {
|
.when('/org/teams', {
|
||||||
template: '<react-container />',
|
template: '<react-container />',
|
||||||
resolve: {
|
resolve: {
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: () => (config.editorsCanAdmin ? [] : ['Editor', 'Admin']),
|
||||||
component: () => TeamList,
|
component: () => TeamList,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -207,7 +207,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
.when('/org/teams/edit/:id/:page?', {
|
.when('/org/teams/edit/:id/:page?', {
|
||||||
template: '<react-container />',
|
template: '<react-container />',
|
||||||
resolve: {
|
resolve: {
|
||||||
roles: () => ['Admin'],
|
roles: () => (config.editorsCanAdmin ? [] : ['Admin']),
|
||||||
component: () => TeamPages,
|
component: () => TeamPages,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -98,3 +98,23 @@ export const dashboardPermissionLevels: DashboardPermissionInfo[] = [
|
|||||||
description: 'Can add/remove permissions and can add, edit and delete dashboards.',
|
description: 'Can add/remove permissions and can add, edit and delete dashboards.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export enum TeamPermissionLevel {
|
||||||
|
Member = 0,
|
||||||
|
Admin = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamPermissionInfo {
|
||||||
|
value: TeamPermissionLevel;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const teamsPermissionLevels: TeamPermissionInfo[] = [
|
||||||
|
{ value: TeamPermissionLevel.Member, label: 'Member', description: 'Is team member' },
|
||||||
|
{
|
||||||
|
value: TeamPermissionLevel.Admin,
|
||||||
|
label: 'Admin',
|
||||||
|
description: 'Can add/remove permissions, members and delete team.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import { TeamPermissionLevel } from './acl';
|
||||||
|
|
||||||
export interface Team {
|
export interface Team {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
email: string;
|
email: string;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
|
permission: TeamPermissionLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamMember {
|
export interface TeamMember {
|
||||||
@ -13,6 +16,7 @@ export interface TeamMember {
|
|||||||
email: string;
|
email: string;
|
||||||
login: string;
|
login: string;
|
||||||
labels: string[];
|
labels: string[];
|
||||||
|
permission: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamGroup {
|
export interface TeamGroup {
|
||||||
|
@ -19,3 +19,9 @@ td.admin-settings-key {
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-list-table {
|
||||||
|
.team-permissions {
|
||||||
|
padding-right: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user