mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 22:22:25 +08:00
Refactor team pages to react & design change (#12574)
* Rewriting team pages in react * teams to react progress * teams: getting team by id returns same DTO as search, needed for AvatarUrl * teams: progress on new team pages * fix: team test * listing team members and removing team members now works * teams: team member page now works * ux: fixed adding team member issue * refactoring TeamPicker to conform to react coding styles better * teams: very close to being done with team page rewrite * minor style tweak * ux: polish to team pages * feature: team pages in react & everything working * fix: removed flickering when changing tabs by always rendering PageHeader
This commit is contained in:
@ -31,7 +31,7 @@ func TestAlertingApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||||
query.Result = []*m.Team{}
|
query.Result = []*m.TeamDTO{}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||||
query.Result = []*m.Team{}
|
query.Result = []*m.TeamDTO{}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
teamResp := []*m.Team{}
|
teamResp := []*m.TeamDTO{}
|
||||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||||
query.Result = teamResp
|
query.Result = teamResp
|
||||||
return nil
|
return nil
|
||||||
|
@ -61,7 +61,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||||
query.Result = []*m.Team{}
|
query.Result = []*m.TeamDTO{}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||||
query.Result = []*m.Team{}
|
query.Result = []*m.TeamDTO{}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -93,5 +93,6 @@ func GetTeamByID(c *m.ReqContext) Response {
|
|||||||
return Error(500, "Failed to get Team", err)
|
return Error(500, "Failed to get Team", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
|
||||||
return JSON(200, &query.Result)
|
return JSON(200, &query.Result)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import (
|
|||||||
func TestTeamApiEndpoint(t *testing.T) {
|
func TestTeamApiEndpoint(t *testing.T) {
|
||||||
Convey("Given two teams", t, func() {
|
Convey("Given two teams", t, func() {
|
||||||
mockResult := models.SearchTeamQueryResult{
|
mockResult := models.SearchTeamQueryResult{
|
||||||
Teams: []*models.SearchTeamDto{
|
Teams: []*models.TeamDTO{
|
||||||
{Name: "team1"},
|
{Name: "team1"},
|
||||||
{Name: "team2"},
|
{Name: "team2"},
|
||||||
},
|
},
|
||||||
|
@ -49,13 +49,13 @@ type DeleteTeamCommand struct {
|
|||||||
type GetTeamByIdQuery struct {
|
type GetTeamByIdQuery struct {
|
||||||
OrgId int64
|
OrgId int64
|
||||||
Id int64
|
Id int64
|
||||||
Result *Team
|
Result *TeamDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetTeamsByUserQuery struct {
|
type GetTeamsByUserQuery struct {
|
||||||
OrgId int64
|
OrgId int64
|
||||||
UserId int64 `json:"userId"`
|
UserId int64 `json:"userId"`
|
||||||
Result []*Team `json:"teams"`
|
Result []*TeamDTO `json:"teams"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchTeamsQuery struct {
|
type SearchTeamsQuery struct {
|
||||||
@ -68,7 +68,7 @@ type SearchTeamsQuery struct {
|
|||||||
Result SearchTeamQueryResult
|
Result SearchTeamQueryResult
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchTeamDto 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"`
|
||||||
@ -78,8 +78,8 @@ type SearchTeamDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SearchTeamQueryResult struct {
|
type SearchTeamQueryResult struct {
|
||||||
TotalCount int64 `json:"totalCount"`
|
TotalCount int64 `json:"totalCount"`
|
||||||
Teams []*SearchTeamDto `json:"teams"`
|
Teams []*TeamDTO `json:"teams"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PerPage int `json:"perPage"`
|
PerPage int `json:"perPage"`
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ type dashboardGuardianImpl struct {
|
|||||||
dashId int64
|
dashId int64
|
||||||
orgId int64
|
orgId int64
|
||||||
acl []*m.DashboardAclInfoDTO
|
acl []*m.DashboardAclInfoDTO
|
||||||
groups []*m.Team
|
teams []*m.TeamDTO
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,15 +186,15 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
|
|||||||
return g.acl, nil
|
return g.acl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) {
|
func (g *dashboardGuardianImpl) getTeams() ([]*m.TeamDTO, error) {
|
||||||
if g.groups != nil {
|
if g.teams != nil {
|
||||||
return g.groups, nil
|
return g.teams, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId}
|
query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId}
|
||||||
err := bus.Dispatch(&query)
|
err := bus.Dispatch(&query)
|
||||||
|
|
||||||
g.groups = query.Result
|
g.teams = query.Result
|
||||||
return query.Result, err
|
return query.Result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ type scenarioContext struct {
|
|||||||
givenUser *m.SignedInUser
|
givenUser *m.SignedInUser
|
||||||
givenDashboardID int64
|
givenDashboardID int64
|
||||||
givenPermissions []*m.DashboardAclInfoDTO
|
givenPermissions []*m.DashboardAclInfoDTO
|
||||||
givenTeams []*m.Team
|
givenTeams []*m.TeamDTO
|
||||||
updatePermissions []*m.DashboardAcl
|
updatePermissions []*m.DashboardAcl
|
||||||
expectedFlags permissionFlags
|
expectedFlags permissionFlags
|
||||||
callerFile string
|
callerFile string
|
||||||
@ -84,11 +84,11 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, per
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
teams := []*m.Team{}
|
teams := []*m.TeamDTO{}
|
||||||
|
|
||||||
for _, p := range permissions {
|
for _, p := range permissions {
|
||||||
if p.TeamId > 0 {
|
if p.TeamId > 0 {
|
||||||
teams = append(teams, &m.Team{Id: p.TeamId})
|
teams = append(teams, &m.TeamDTO{Id: p.TeamId})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,16 @@ func init() {
|
|||||||
bus.AddHandler("sql", GetTeamMembers)
|
bus.AddHandler("sql", GetTeamMembers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTeamSelectSqlBase() 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
|
||||||
|
FROM team as team `
|
||||||
|
}
|
||||||
|
|
||||||
func CreateTeam(cmd *m.CreateTeamCommand) error {
|
func CreateTeam(cmd *m.CreateTeamCommand) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
|
|
||||||
@ -130,21 +140,15 @@ func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession
|
|||||||
|
|
||||||
func SearchTeams(query *m.SearchTeamsQuery) error {
|
func SearchTeams(query *m.SearchTeamsQuery) error {
|
||||||
query.Result = m.SearchTeamQueryResult{
|
query.Result = m.SearchTeamQueryResult{
|
||||||
Teams: make([]*m.SearchTeamDto, 0),
|
Teams: make([]*m.TeamDTO, 0),
|
||||||
}
|
}
|
||||||
queryWithWildcards := "%" + query.Query + "%"
|
queryWithWildcards := "%" + query.Query + "%"
|
||||||
|
|
||||||
var sql bytes.Buffer
|
var sql bytes.Buffer
|
||||||
params := make([]interface{}, 0)
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
sql.WriteString(`select
|
sql.WriteString(getTeamSelectSqlBase())
|
||||||
team.id as id,
|
sql.WriteString(` WHERE team.org_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
|
|
||||||
from team as team
|
|
||||||
where team.org_id = ?`)
|
|
||||||
|
|
||||||
params = append(params, query.OrgId)
|
params = append(params, query.OrgId)
|
||||||
|
|
||||||
@ -186,8 +190,14 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetTeamById(query *m.GetTeamByIdQuery) error {
|
func GetTeamById(query *m.GetTeamByIdQuery) error {
|
||||||
var team m.Team
|
var sql bytes.Buffer
|
||||||
exists, err := x.Where("org_id=? and id=?", query.OrgId, query.Id).Get(&team)
|
|
||||||
|
sql.WriteString(getTeamSelectSqlBase())
|
||||||
|
sql.WriteString(` WHERE team.org_id = ? and team.id = ?`)
|
||||||
|
|
||||||
|
var team m.TeamDTO
|
||||||
|
exists, err := x.Sql(sql.String(), query.OrgId, query.Id).Get(&team)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -202,13 +212,15 @@ func GetTeamById(query *m.GetTeamByIdQuery) error {
|
|||||||
|
|
||||||
// GetTeamsByUser is used by the Guardian when checking a users' permissions
|
// GetTeamsByUser is used by the Guardian when checking a users' permissions
|
||||||
func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
|
func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
|
||||||
query.Result = make([]*m.Team, 0)
|
query.Result = make([]*m.TeamDTO, 0)
|
||||||
|
|
||||||
sess := x.Table("team")
|
var sql bytes.Buffer
|
||||||
sess.Join("INNER", "team_member", "team.id=team_member.team_id")
|
|
||||||
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
|
|
||||||
|
|
||||||
err := sess.Find(&query.Result)
|
sql.WriteString(getTeamSelectSqlBase())
|
||||||
|
sql.WriteString(` INNER JOIN team_member on team.id = team_member.team_id`)
|
||||||
|
sql.WriteString(` WHERE team.org_id = ? and team_member.user_id = ?`)
|
||||||
|
|
||||||
|
err := x.Sql(sql.String(), query.OrgId, query.UserId).Find(&query.Result)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
|||||||
<PageHeader model={nav as any} />
|
<PageHeader model={nav as any} />
|
||||||
<div className="page-container page-body">
|
<div className="page-container page-body">
|
||||||
<div className="page-action-bar">
|
<div className="page-action-bar">
|
||||||
<h2 className="d-inline-block">Folder Permissions</h2>
|
<h3 className="page-sub-heading">Folder Permissions</h3>
|
||||||
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
|
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
|
||||||
<i className="gicon gicon-question gicon--has-hover" />
|
<i className="gicon gicon-question gicon--has-hover" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -68,7 +68,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<SlideDown in={permissions.isAddPermissionsVisible}>
|
<SlideDown in={permissions.isAddPermissionsVisible}>
|
||||||
<AddPermissions permissions={permissions} backendSrv={backendSrv} />
|
<AddPermissions permissions={permissions} />
|
||||||
</SlideDown>
|
</SlideDown>
|
||||||
<Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
|
<Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
|
||||||
</div>
|
</div>
|
||||||
|
149
public/app/containers/Teams/TeamGroupSync.tsx
Normal file
149
public/app/containers/Teams/TeamGroupSync.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { ITeam, ITeamGroup } from 'app/stores/TeamsStore/TeamsStore';
|
||||||
|
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||||
|
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
team: ITeam;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isAdding: boolean;
|
||||||
|
newGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`;
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class TeamGroupSync extends React.Component<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { isAdding: false, newGroupId: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.team.loadGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGroup(group: ITeamGroup) {
|
||||||
|
return (
|
||||||
|
<tr key={group.groupId}>
|
||||||
|
<td>{group.groupId}</td>
|
||||||
|
<td style={{ width: '1%' }}>
|
||||||
|
<a className="btn btn-danger btn-mini" onClick={() => this.onRemoveGroup(group)}>
|
||||||
|
<i className="fa fa-remove" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleAdding = () => {
|
||||||
|
this.setState({ isAdding: !this.state.isAdding });
|
||||||
|
};
|
||||||
|
|
||||||
|
onNewGroupIdChanged = evt => {
|
||||||
|
this.setState({ newGroupId: evt.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddGroup = () => {
|
||||||
|
this.props.team.addGroup(this.state.newGroupId);
|
||||||
|
this.setState({ isAdding: false, newGroupId: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveGroup = (group: ITeamGroup) => {
|
||||||
|
this.props.team.removeGroup(group.groupId);
|
||||||
|
};
|
||||||
|
|
||||||
|
isNewGroupValid() {
|
||||||
|
return this.state.newGroupId.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isAdding, newGroupId } = this.state;
|
||||||
|
const groups = this.props.team.groups.values();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="page-action-bar">
|
||||||
|
<h3 className="page-sub-heading">External group sync</h3>
|
||||||
|
<Tooltip className="page-sub-heading-icon" placement="auto" content={headerTooltip}>
|
||||||
|
<i className="gicon gicon-question gicon--has-hover" />
|
||||||
|
</Tooltip>
|
||||||
|
<div className="page-action-bar__spacer" />
|
||||||
|
{groups.length > 0 && (
|
||||||
|
<button className="btn btn-success pull-right" onClick={this.onToggleAdding}>
|
||||||
|
<i className="fa fa-plus" /> Add group
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SlideDown in={isAdding}>
|
||||||
|
<div className="cta-form">
|
||||||
|
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
|
||||||
|
<i className="fa fa-close" />
|
||||||
|
</button>
|
||||||
|
<h5>Add External Group</h5>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input width-30"
|
||||||
|
value={newGroupId}
|
||||||
|
onChange={this.onNewGroupIdChanged}
|
||||||
|
placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<button
|
||||||
|
className="btn btn-success gf-form-btn"
|
||||||
|
onClick={this.onAddGroup}
|
||||||
|
type="submit"
|
||||||
|
disabled={!this.isNewGroupValid()}
|
||||||
|
>
|
||||||
|
Add group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SlideDown>
|
||||||
|
|
||||||
|
{groups.length === 0 &&
|
||||||
|
!isAdding && (
|
||||||
|
<div className="empty-list-cta">
|
||||||
|
<div className="empty-list-cta__title">There are no external groups to sync with</div>
|
||||||
|
<button onClick={this.onToggleAdding} className="empty-list-cta__button btn btn-xlarge btn-success">
|
||||||
|
<i className="gicon gicon-add-team" />
|
||||||
|
Add Group
|
||||||
|
</button>
|
||||||
|
<div className="empty-list-cta__pro-tip">
|
||||||
|
<i className="fa fa-rocket" /> {headerTooltip}
|
||||||
|
<a className="text-link empty-list-cta__pro-tip-link" href="asd" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{groups.length > 0 && (
|
||||||
|
<div className="admin-list-table">
|
||||||
|
<table className="filter-table filter-table--hover form-inline">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>External Group ID</th>
|
||||||
|
<th style={{ width: '1%' }} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{groups.map(group => this.renderGroup(group))}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(module)(TeamGroupSync);
|
125
public/app/containers/Teams/TeamList.tsx
Normal file
125
public/app/containers/Teams/TeamList.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { inject, observer } from 'mobx-react';
|
||||||
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
|
import { NavStore } from 'app/stores/NavStore/NavStore';
|
||||||
|
import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore';
|
||||||
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nav: typeof NavStore.Type;
|
||||||
|
teams: typeof TeamsStore.Type;
|
||||||
|
backendSrv: BackendSrv;
|
||||||
|
}
|
||||||
|
|
||||||
|
@inject('nav', 'teams')
|
||||||
|
@observer
|
||||||
|
export class TeamList extends React.Component<Props, any> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.props.nav.load('cfg', 'teams');
|
||||||
|
this.fetchTeams();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchTeams() {
|
||||||
|
this.props.teams.loadTeams();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTeam(team: ITeam) {
|
||||||
|
appEvents.emit('confirm-modal', {
|
||||||
|
title: 'Delete',
|
||||||
|
text: 'Are you sure you want to delete Team ' + team.name + '?',
|
||||||
|
yesText: 'Delete',
|
||||||
|
icon: 'fa-warning',
|
||||||
|
onConfirm: () => {
|
||||||
|
this.deleteTeamConfirmed(team);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTeamConfirmed(team) {
|
||||||
|
this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchQueryChange = evt => {
|
||||||
|
this.props.teams.setSearchQuery(evt.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTeamMember(team: ITeam): JSX.Element {
|
||||||
|
let teamUrl = `org/teams/edit/${team.id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={team.id}>
|
||||||
|
<td className="width-4 text-center link-td">
|
||||||
|
<a href={teamUrl}>
|
||||||
|
<img className="filter-table__avatar" src={team.avatarUrl} />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td className="link-td">
|
||||||
|
<a href={teamUrl}>{team.name}</a>
|
||||||
|
</td>
|
||||||
|
<td className="link-td">
|
||||||
|
<a href={teamUrl}>{team.email}</a>
|
||||||
|
</td>
|
||||||
|
<td className="link-td">
|
||||||
|
<a href={teamUrl}>{team.memberCount}</a>
|
||||||
|
</td>
|
||||||
|
<td className="text-right">
|
||||||
|
<a onClick={() => this.deleteTeam(team)} className="btn btn-danger btn-small">
|
||||||
|
<i className="fa fa-remove" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { nav, teams } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader model={nav as any} />
|
||||||
|
<div className="page-container page-body">
|
||||||
|
<div className="page-action-bar">
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<label className="gf-form--has-input-icon gf-form--grow">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input"
|
||||||
|
placeholder="Search teams"
|
||||||
|
value={teams.search}
|
||||||
|
onChange={this.onSearchQueryChange}
|
||||||
|
/>
|
||||||
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="page-action-bar__spacer" />
|
||||||
|
|
||||||
|
<a className="btn btn-success" href="org/teams/new">
|
||||||
|
<i className="fa fa-plus" /> 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={{ width: '1%' }} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{teams.filteredTeams.map(team => this.renderTeamMember(team))}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(module)(TeamList);
|
144
public/app/containers/Teams/TeamMembers.tsx
Normal file
144
public/app/containers/Teams/TeamMembers.tsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { ITeam, ITeamMember } from 'app/stores/TeamsStore/TeamsStore';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||||
|
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
team: ITeam;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isAdding: boolean;
|
||||||
|
newTeamMember?: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class TeamMembers extends React.Component<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { isAdding: false, newTeamMember: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.team.loadMembers();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchQueryChange = evt => {
|
||||||
|
this.props.team.setSearchQuery(evt.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
removeMember(member: ITeamMember) {
|
||||||
|
appEvents.emit('confirm-modal', {
|
||||||
|
title: 'Remove Member',
|
||||||
|
text: 'Are you sure you want to remove ' + member.login + ' from this group?',
|
||||||
|
yesText: 'Remove',
|
||||||
|
icon: 'fa-warning',
|
||||||
|
onConfirm: () => {
|
||||||
|
this.removeMemberConfirmed(member);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMemberConfirmed(member: ITeamMember) {
|
||||||
|
this.props.team.removeMember(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMember(member: ITeamMember) {
|
||||||
|
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>
|
||||||
|
<td style={{ width: '1%' }}>
|
||||||
|
<a onClick={() => this.removeMember(member)} className="btn btn-danger btn-mini">
|
||||||
|
<i className="fa fa-remove" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleAdding = () => {
|
||||||
|
this.setState({ isAdding: !this.state.isAdding });
|
||||||
|
};
|
||||||
|
|
||||||
|
onUserSelected = (user: User) => {
|
||||||
|
this.setState({ newTeamMember: user });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddUserToTeam = async () => {
|
||||||
|
await this.props.team.addMember(this.state.newTeamMember.id);
|
||||||
|
await this.props.team.loadMembers();
|
||||||
|
this.setState({ newTeamMember: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { newTeamMember, isAdding } = this.state;
|
||||||
|
const members = this.props.team.members.values();
|
||||||
|
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="page-action-bar">
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<label className="gf-form--has-input-icon gf-form--grow">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input"
|
||||||
|
placeholder="Search members"
|
||||||
|
value={''}
|
||||||
|
onChange={this.onSearchQueryChange}
|
||||||
|
/>
|
||||||
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="page-action-bar__spacer" />
|
||||||
|
|
||||||
|
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
|
||||||
|
<i className="fa fa-plus" /> Add a member
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SlideDown in={isAdding}>
|
||||||
|
<div className="cta-form">
|
||||||
|
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
|
||||||
|
<i className="fa fa-close" />
|
||||||
|
</button>
|
||||||
|
<h5>Add Team Member</h5>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<UserPicker onSelected={this.onUserSelected} className="width-30" value={newTeamMemberValue} />
|
||||||
|
|
||||||
|
{this.state.newTeamMember && (
|
||||||
|
<button className="btn btn-success gf-form-btn" type="submit" onClick={this.onAddUserToTeam}>
|
||||||
|
Add to team
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SlideDown>
|
||||||
|
|
||||||
|
<div className="admin-list-table">
|
||||||
|
<table className="filter-table filter-table--hover form-inline">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th style={{ width: '1%' }} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{members.map(member => this.renderMember(member))}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(module)(TeamMembers);
|
77
public/app/containers/Teams/TeamPages.tsx
Normal file
77
public/app/containers/Teams/TeamPages.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { inject, observer } from 'mobx-react';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
|
import { NavStore } from 'app/stores/NavStore/NavStore';
|
||||||
|
import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore';
|
||||||
|
import { ViewStore } from 'app/stores/ViewStore/ViewStore';
|
||||||
|
import TeamMembers from './TeamMembers';
|
||||||
|
import TeamSettings from './TeamSettings';
|
||||||
|
import TeamGroupSync from './TeamGroupSync';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nav: typeof NavStore.Type;
|
||||||
|
teams: typeof TeamsStore.Type;
|
||||||
|
view: typeof ViewStore.Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@inject('nav', 'teams', 'view')
|
||||||
|
@observer
|
||||||
|
export class TeamPages extends React.Component<Props, any> {
|
||||||
|
isSyncEnabled: boolean;
|
||||||
|
currentPage: string;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.isSyncEnabled = config.buildInfo.isEnterprise;
|
||||||
|
this.currentPage = this.getCurrentPage();
|
||||||
|
|
||||||
|
this.loadTeam();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTeam() {
|
||||||
|
const { teams, nav, view } = this.props;
|
||||||
|
|
||||||
|
await teams.loadById(view.routeParams.get('id'));
|
||||||
|
|
||||||
|
nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentTeam(): ITeam {
|
||||||
|
const { teams, view } = this.props;
|
||||||
|
return teams.map.get(view.routeParams.get('id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPage() {
|
||||||
|
const pages = ['members', 'settings', 'groupsync'];
|
||||||
|
const currentPage = this.props.view.routeParams.get('page');
|
||||||
|
return _.includes(pages, currentPage) ? currentPage : pages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { nav } = this.props;
|
||||||
|
const currentTeam = this.getCurrentTeam();
|
||||||
|
|
||||||
|
if (!nav.main) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader model={nav as any} />
|
||||||
|
{currentTeam && (
|
||||||
|
<div className="page-container page-body">
|
||||||
|
{this.currentPage === 'members' && <TeamMembers team={currentTeam} />}
|
||||||
|
{this.currentPage === 'settings' && <TeamSettings team={currentTeam} />}
|
||||||
|
{this.currentPage === 'groupsync' && this.isSyncEnabled && <TeamGroupSync team={currentTeam} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(module)(TeamPages);
|
69
public/app/containers/Teams/TeamSettings.tsx
Normal file
69
public/app/containers/Teams/TeamSettings.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { ITeam } from 'app/stores/TeamsStore/TeamsStore';
|
||||||
|
import { Label } from 'app/core/components/Forms/Forms';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
team: ITeam;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class TeamSettings extends React.Component<Props, any> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeName = evt => {
|
||||||
|
this.props.team.setName(evt.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeEmail = evt => {
|
||||||
|
this.props.team.setEmail(evt.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdate = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.props.team.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="page-sub-heading">Team Settings</h3>
|
||||||
|
<form name="teamDetailsForm" className="gf-form-group">
|
||||||
|
<div className="gf-form max-width-30">
|
||||||
|
<Label>Name</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={this.props.team.name}
|
||||||
|
className="gf-form-input max-width-22"
|
||||||
|
onChange={this.onChangeName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form max-width-30">
|
||||||
|
<Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="gf-form-input max-width-22"
|
||||||
|
value={this.props.team.email}
|
||||||
|
placeholder="team@email.com"
|
||||||
|
onChange={this.onChangeEmail}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form-button-row">
|
||||||
|
<button type="submit" className="btn btn-success" onClick={this.onUpdate}>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(module)(TeamSettings);
|
@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
|||||||
import LoginBackground from './components/Login/LoginBackground';
|
import LoginBackground from './components/Login/LoginBackground';
|
||||||
import { SearchResult } from './components/search/SearchResult';
|
import { SearchResult } from './components/search/SearchResult';
|
||||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||||
import UserPicker from './components/Picker/UserPicker';
|
|
||||||
import DashboardPermissions from './components/Permissions/DashboardPermissions';
|
import DashboardPermissions from './components/Permissions/DashboardPermissions';
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
@ -19,6 +18,5 @@ export function registerAngularDirectives() {
|
|||||||
['onSelect', { watchDepth: 'reference' }],
|
['onSelect', { watchDepth: 'reference' }],
|
||||||
['tagOptions', { watchDepth: 'reference' }],
|
['tagOptions', { watchDepth: 'reference' }],
|
||||||
]);
|
]);
|
||||||
react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']);
|
|
||||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']);
|
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']);
|
||||||
}
|
}
|
||||||
|
21
public/app/core/components/Forms/Forms.tsx
Normal file
21
public/app/core/components/Forms/Forms.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React, { SFC, ReactNode } from 'react';
|
||||||
|
import Tooltip from '../Tooltip/Tooltip';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tooltip?: string;
|
||||||
|
for?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Label: SFC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<span className="gf-form-label width-10">
|
||||||
|
<span>{props.children}</span>
|
||||||
|
{props.tooltip && (
|
||||||
|
<Tooltip className="gf-form-help-icon--right-normal" placement="auto" content="hello">
|
||||||
|
<i className="gicon gicon-question gicon--has-hover" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
@ -1,32 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
import AddPermissions from './AddPermissions';
|
import AddPermissions from './AddPermissions';
|
||||||
import { RootStore } from 'app/stores/RootStore/RootStore';
|
import { RootStore } from 'app/stores/RootStore/RootStore';
|
||||||
import { backendSrv } from 'test/mocks/common';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
jest.mock('app/core/services/backend_srv', () => ({
|
||||||
|
getBackendSrv: () => {
|
||||||
|
return {
|
||||||
|
get: () => {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' },
|
||||||
|
{ id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
post: jest.fn(() => Promise.resolve({})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AddPermissions', () => {
|
describe('AddPermissions', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let store;
|
let store;
|
||||||
let instance;
|
let instance;
|
||||||
|
let backendSrv: any = getBackendSrv();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
backendSrv.get.mockReturnValue(
|
store = RootStore.create({}, { backendSrv: backendSrv });
|
||||||
Promise.resolve([
|
wrapper = shallow(<AddPermissions permissions={store.permissions} />);
|
||||||
{ id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' },
|
|
||||||
{ id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
backendSrv.post = jest.fn(() => Promise.resolve({}));
|
|
||||||
|
|
||||||
store = RootStore.create(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
backendSrv: backendSrv,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper = shallow(<AddPermissions permissions={store.permissions} backendSrv={backendSrv} />);
|
|
||||||
instance = wrapper.instance();
|
instance = wrapper.instance();
|
||||||
return store.permissions.load(1, true, false);
|
return store.permissions.load(1, true, false);
|
||||||
});
|
});
|
||||||
@ -43,8 +43,8 @@ describe('AddPermissions', () => {
|
|||||||
login: 'user2',
|
login: 'user2',
|
||||||
};
|
};
|
||||||
|
|
||||||
instance.typeChanged(evt);
|
instance.onTypeChanged(evt);
|
||||||
instance.userPicked(userItem);
|
instance.onUserSelected(userItem);
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
@ -70,8 +70,8 @@ describe('AddPermissions', () => {
|
|||||||
name: 'ug1',
|
name: 'ug1',
|
||||||
};
|
};
|
||||||
|
|
||||||
instance.typeChanged(evt);
|
instance.onTypeChanged(evt);
|
||||||
instance.teamPicked(teamItem);
|
instance.onTeamSelected(teamItem);
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
|
@ -1,24 +1,19 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore';
|
import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore';
|
||||||
import UserPicker, { User } from 'app/core/components/Picker/UserPicker';
|
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
||||||
import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker';
|
import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker';
|
||||||
import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
|
import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
|
||||||
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
|
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
|
||||||
|
|
||||||
export interface IProps {
|
export interface Props {
|
||||||
permissions: any;
|
permissions: any;
|
||||||
backendSrv: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class AddPermissions extends Component<IProps, any> {
|
class AddPermissions extends Component<Props, any> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.userPicked = this.userPicked.bind(this);
|
|
||||||
this.teamPicked = this.teamPicked.bind(this);
|
|
||||||
this.permissionPicked = this.permissionPicked.bind(this);
|
|
||||||
this.typeChanged = this.typeChanged.bind(this);
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
@ -26,49 +21,49 @@ class AddPermissions extends Component<IProps, any> {
|
|||||||
permissions.resetNewType();
|
permissions.resetNewType();
|
||||||
}
|
}
|
||||||
|
|
||||||
typeChanged(evt) {
|
onTypeChanged = evt => {
|
||||||
const { value } = evt.target;
|
const { value } = evt.target;
|
||||||
const { permissions } = this.props;
|
const { permissions } = this.props;
|
||||||
|
|
||||||
permissions.setNewType(value);
|
permissions.setNewType(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
userPicked(user: User) {
|
onUserSelected = (user: User) => {
|
||||||
const { permissions } = this.props;
|
const { permissions } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
permissions.newItem.setUser(null, null);
|
permissions.newItem.setUser(null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return permissions.newItem.setUser(user.id, user.login, user.avatarUrl);
|
return permissions.newItem.setUser(user.id, user.login, user.avatarUrl);
|
||||||
}
|
};
|
||||||
|
|
||||||
teamPicked(team: Team) {
|
onTeamSelected = (team: Team) => {
|
||||||
const { permissions } = this.props;
|
const { permissions } = this.props;
|
||||||
if (!team) {
|
if (!team) {
|
||||||
permissions.newItem.setTeam(null, null);
|
permissions.newItem.setTeam(null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl);
|
return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl);
|
||||||
}
|
};
|
||||||
|
|
||||||
permissionPicked(permission: OptionWithDescription) {
|
onPermissionChanged = (permission: OptionWithDescription) => {
|
||||||
const { permissions } = this.props;
|
const { permissions } = this.props;
|
||||||
return permissions.newItem.setPermission(permission.value);
|
return permissions.newItem.setPermission(permission.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
resetNewType() {
|
resetNewType() {
|
||||||
const { permissions } = this.props;
|
const { permissions } = this.props;
|
||||||
return permissions.resetNewType();
|
return permissions.resetNewType();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit(evt) {
|
onSubmit = evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { permissions } = this.props;
|
const { permissions } = this.props;
|
||||||
permissions.addStoreItem();
|
permissions.addStoreItem();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { permissions, backendSrv } = this.props;
|
const { permissions } = this.props;
|
||||||
const newItem = permissions.newItem;
|
const newItem = permissions.newItem;
|
||||||
const pickerClassName = 'width-20';
|
const pickerClassName = 'width-20';
|
||||||
|
|
||||||
@ -79,12 +74,12 @@ class AddPermissions extends Component<IProps, any> {
|
|||||||
<button className="cta-form__close btn btn-transparent" onClick={permissions.hideAddPermissions}>
|
<button className="cta-form__close btn btn-transparent" onClick={permissions.hideAddPermissions}>
|
||||||
<i className="fa fa-close" />
|
<i className="fa fa-close" />
|
||||||
</button>
|
</button>
|
||||||
<form name="addPermission" onSubmit={this.handleSubmit}>
|
<form name="addPermission" onSubmit={this.onSubmit}>
|
||||||
<h6>Add Permission For</h6>
|
<h5>Add Permission For</h5>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<div className="gf-form-select-wrapper">
|
<div className="gf-form-select-wrapper">
|
||||||
<select className="gf-form-input gf-size-auto" value={newItem.type} onChange={this.typeChanged}>
|
<select className="gf-form-input gf-size-auto" value={newItem.type} onChange={this.onTypeChanged}>
|
||||||
{aclTypes.map((option, idx) => {
|
{aclTypes.map((option, idx) => {
|
||||||
return (
|
return (
|
||||||
<option key={idx} value={option.value}>
|
<option key={idx} value={option.value}>
|
||||||
@ -98,30 +93,20 @@ class AddPermissions extends Component<IProps, any> {
|
|||||||
|
|
||||||
{newItem.type === 'User' ? (
|
{newItem.type === 'User' ? (
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<UserPicker
|
<UserPicker onSelected={this.onUserSelected} value={newItem.userId} className={pickerClassName} />
|
||||||
backendSrv={backendSrv}
|
|
||||||
handlePicked={this.userPicked}
|
|
||||||
value={newItem.userId}
|
|
||||||
className={pickerClassName}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{newItem.type === 'Group' ? (
|
{newItem.type === 'Group' ? (
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<TeamPicker
|
<TeamPicker onSelected={this.onTeamSelected} value={newItem.teamId} className={pickerClassName} />
|
||||||
backendSrv={backendSrv}
|
|
||||||
handlePicked={this.teamPicked}
|
|
||||||
value={newItem.teamId}
|
|
||||||
className={pickerClassName}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<DescriptionPicker
|
<DescriptionPicker
|
||||||
optionsWithDesc={permissionOptions}
|
optionsWithDesc={permissionOptions}
|
||||||
handlePicked={this.permissionPicked}
|
onSelected={this.onPermissionChanged}
|
||||||
value={newItem.permission}
|
value={newItem.permission}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
className={'gf-form-input--form-dropdown-right'}
|
className={'gf-form-input--form-dropdown-right'}
|
||||||
|
@ -8,13 +8,14 @@ import AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
|||||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||||
import { FolderInfo } from './FolderInfo';
|
import { FolderInfo } from './FolderInfo';
|
||||||
|
|
||||||
export interface IProps {
|
export interface Props {
|
||||||
dashboardId: number;
|
dashboardId: number;
|
||||||
folder?: FolderInfo;
|
folder?: FolderInfo;
|
||||||
backendSrv: any;
|
backendSrv: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class DashboardPermissions extends Component<IProps, any> {
|
class DashboardPermissions extends Component<Props, any> {
|
||||||
permissions: any;
|
permissions: any;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -53,7 +54,7 @@ class DashboardPermissions extends Component<IProps, any> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SlideDown in={this.permissions.isAddPermissionsVisible}>
|
<SlideDown in={this.permissions.isAddPermissionsVisible}>
|
||||||
<AddPermissions permissions={this.permissions} backendSrv={backendSrv} />
|
<AddPermissions permissions={this.permissions} />
|
||||||
</SlideDown>
|
</SlideDown>
|
||||||
<Permissions
|
<Permissions
|
||||||
permissions={this.permissions}
|
permissions={this.permissions}
|
||||||
|
@ -25,7 +25,7 @@ export default class DisabledPermissionListItem extends Component<IProps, any> {
|
|||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<DescriptionPicker
|
<DescriptionPicker
|
||||||
optionsWithDesc={permissionOptions}
|
optionsWithDesc={permissionOptions}
|
||||||
handlePicked={() => {}}
|
onSelected={() => {}}
|
||||||
value={item.permission}
|
value={item.permission}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className={'gf-form-input--form-dropdown-right'}
|
className={'gf-form-input--form-dropdown-right'}
|
||||||
|
@ -68,7 +68,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
|
|||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<DescriptionPicker
|
<DescriptionPicker
|
||||||
optionsWithDesc={permissionOptions}
|
optionsWithDesc={permissionOptions}
|
||||||
handlePicked={handleChangePermission}
|
onSelected={handleChangePermission}
|
||||||
value={item.permission}
|
value={item.permission}
|
||||||
disabled={item.inherited}
|
disabled={item.inherited}
|
||||||
className={'gf-form-input--form-dropdown-right'}
|
className={'gf-form-input--form-dropdown-right'}
|
||||||
|
@ -2,9 +2,9 @@ import React, { Component } from 'react';
|
|||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import DescriptionOption from './DescriptionOption';
|
import DescriptionOption from './DescriptionOption';
|
||||||
|
|
||||||
export interface IProps {
|
export interface Props {
|
||||||
optionsWithDesc: OptionWithDescription[];
|
optionsWithDesc: OptionWithDescription[];
|
||||||
handlePicked: (permission) => void;
|
onSelected: (permission) => void;
|
||||||
value: number;
|
value: number;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -16,14 +16,14 @@ export interface OptionWithDescription {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DescriptionPicker extends Component<IProps, any> {
|
class DescriptionPicker extends Component<Props, any> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {};
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { optionsWithDesc, handlePicked, value, disabled, className } = this.props;
|
const { optionsWithDesc, onSelected, value, disabled, className } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="permissions-picker">
|
<div className="permissions-picker">
|
||||||
@ -34,7 +34,7 @@ class DescriptionPicker extends Component<IProps, any> {
|
|||||||
clearable={false}
|
clearable={false}
|
||||||
labelKey="label"
|
labelKey="label"
|
||||||
options={optionsWithDesc}
|
options={optionsWithDesc}
|
||||||
onChange={handlePicked}
|
onChange={onSelected}
|
||||||
className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
||||||
optionComponent={DescriptionOption}
|
optionComponent={DescriptionOption}
|
||||||
placeholder="Choose"
|
placeholder="Choose"
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import TeamPicker from './TeamPicker';
|
import { TeamPicker } from './TeamPicker';
|
||||||
|
|
||||||
const model = {
|
jest.mock('app/core/services/backend_srv', () => ({
|
||||||
backendSrv: {
|
getBackendSrv: () => {
|
||||||
get: () => {
|
return {
|
||||||
return new Promise((resolve, reject) => {});
|
get: () => {
|
||||||
},
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
handlePicked: () => {},
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
describe('TeamPicker', () => {
|
describe('TeamPicker', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const tree = renderer.create(<TeamPicker {...model} />).toJSON();
|
const props = {
|
||||||
|
onSelected: () => {},
|
||||||
|
};
|
||||||
|
const tree = renderer.create(<TeamPicker {...props} />).toJSON();
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import PickerOption from './PickerOption';
|
import PickerOption from './PickerOption';
|
||||||
import withPicker from './withPicker';
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
export interface IProps {
|
export interface Props {
|
||||||
backendSrv: any;
|
onSelected: (team: Team) => void;
|
||||||
isLoading: boolean;
|
|
||||||
toggleLoading: any;
|
|
||||||
handlePicked: (user) => void;
|
|
||||||
value?: string;
|
value?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Team {
|
export interface Team {
|
||||||
id: number;
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
@ -20,13 +21,12 @@ export interface Team {
|
|||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TeamPicker extends Component<IProps, any> {
|
export class TeamPicker extends Component<Props, State> {
|
||||||
debouncedSearch: any;
|
debouncedSearch: any;
|
||||||
backendSrv: any;
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {};
|
this.state = { isLoading: false };
|
||||||
this.search = this.search.bind(this);
|
this.search = this.search.bind(this);
|
||||||
|
|
||||||
this.debouncedSearch = debounce(this.search, 300, {
|
this.debouncedSearch = debounce(this.search, 300, {
|
||||||
@ -36,9 +36,9 @@ class TeamPicker extends Component<IProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search(query?: string) {
|
search(query?: string) {
|
||||||
const { toggleLoading, backendSrv } = this.props;
|
const backendSrv = getBackendSrv();
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
|
||||||
toggleLoading(true);
|
|
||||||
return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => {
|
return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => {
|
||||||
const teams = result.teams.map(team => {
|
const teams = result.teams.map(team => {
|
||||||
return {
|
return {
|
||||||
@ -49,18 +49,18 @@ class TeamPicker extends Component<IProps, any> {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
toggleLoading(false);
|
this.setState({ isLoading: false });
|
||||||
return { options: teams };
|
return { options: teams };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async;
|
const { onSelected, value, className } = this.props;
|
||||||
const { isLoading, handlePicked, value, className } = this.props;
|
const { isLoading } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-picker">
|
<div className="user-picker">
|
||||||
<AsyncComponent
|
<Select.Async
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
multi={false}
|
multi={false}
|
||||||
labelKey="label"
|
labelKey="label"
|
||||||
@ -69,10 +69,10 @@ class TeamPicker extends Component<IProps, any> {
|
|||||||
loadOptions={this.debouncedSearch}
|
loadOptions={this.debouncedSearch}
|
||||||
loadingPlaceholder="Loading..."
|
loadingPlaceholder="Loading..."
|
||||||
noResultsText="No teams found"
|
noResultsText="No teams found"
|
||||||
onChange={handlePicked}
|
onChange={onSelected}
|
||||||
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
||||||
optionComponent={PickerOption}
|
optionComponent={PickerOption}
|
||||||
placeholder="Choose"
|
placeholder="Select a team"
|
||||||
value={value}
|
value={value}
|
||||||
autosize={true}
|
autosize={true}
|
||||||
/>
|
/>
|
||||||
@ -80,5 +80,3 @@ class TeamPicker extends Component<IProps, any> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withPicker(TeamPicker);
|
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import UserPicker from './UserPicker';
|
import { UserPicker } from './UserPicker';
|
||||||
|
|
||||||
const model = {
|
jest.mock('app/core/services/backend_srv', () => ({
|
||||||
backendSrv: {
|
getBackendSrv: () => {
|
||||||
get: () => {
|
return {
|
||||||
return new Promise((resolve, reject) => {});
|
get: () => {
|
||||||
},
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
handlePicked: () => {},
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
describe('UserPicker', () => {
|
describe('UserPicker', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const tree = renderer.create(<UserPicker {...model} />).toJSON();
|
const tree = renderer.create(<UserPicker onSelected={() => {}} />).toJSON();
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import PickerOption from './PickerOption';
|
import PickerOption from './PickerOption';
|
||||||
import withPicker from './withPicker';
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
export interface IProps {
|
export interface Props {
|
||||||
backendSrv: any;
|
onSelected: (user: User) => void;
|
||||||
isLoading: boolean;
|
|
||||||
toggleLoading: any;
|
|
||||||
handlePicked: (user) => void;
|
|
||||||
value?: string;
|
value?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
@ -20,13 +21,12 @@ export interface User {
|
|||||||
login: string;
|
login: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserPicker extends Component<IProps, any> {
|
export class UserPicker extends Component<Props, State> {
|
||||||
debouncedSearch: any;
|
debouncedSearch: any;
|
||||||
backendSrv: any;
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {};
|
this.state = { isLoading: false };
|
||||||
this.search = this.search.bind(this);
|
this.search = this.search.bind(this);
|
||||||
|
|
||||||
this.debouncedSearch = debounce(this.search, 300, {
|
this.debouncedSearch = debounce(this.search, 300, {
|
||||||
@ -36,29 +36,34 @@ class UserPicker extends Component<IProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search(query?: string) {
|
search(query?: string) {
|
||||||
const { toggleLoading, backendSrv } = this.props;
|
const backendSrv = getBackendSrv();
|
||||||
|
|
||||||
toggleLoading(true);
|
this.setState({ isLoading: true });
|
||||||
return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => {
|
|
||||||
const users = result.map(user => {
|
return backendSrv
|
||||||
|
.get(`/api/org/users?query=${query}&limit=10`)
|
||||||
|
.then(result => {
|
||||||
return {
|
return {
|
||||||
id: user.userId,
|
options: result.map(user => ({
|
||||||
label: `${user.login} - ${user.email}`,
|
id: user.userId,
|
||||||
avatarUrl: user.avatarUrl,
|
label: `${user.login} - ${user.email}`,
|
||||||
login: user.login,
|
avatarUrl: user.avatarUrl,
|
||||||
|
login: user.login,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ isLoading: false });
|
||||||
});
|
});
|
||||||
toggleLoading(false);
|
|
||||||
return { options: users };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async;
|
const { value, className } = this.props;
|
||||||
const { isLoading, handlePicked, value, className } = this.props;
|
const { isLoading } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-picker">
|
<div className="user-picker">
|
||||||
<AsyncComponent
|
<Select.Async
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
multi={false}
|
multi={false}
|
||||||
labelKey="label"
|
labelKey="label"
|
||||||
@ -67,10 +72,10 @@ class UserPicker extends Component<IProps, any> {
|
|||||||
loadOptions={this.debouncedSearch}
|
loadOptions={this.debouncedSearch}
|
||||||
loadingPlaceholder="Loading..."
|
loadingPlaceholder="Loading..."
|
||||||
noResultsText="No users found"
|
noResultsText="No users found"
|
||||||
onChange={handlePicked}
|
onChange={this.props.onSelected}
|
||||||
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
||||||
optionComponent={PickerOption}
|
optionComponent={PickerOption}
|
||||||
placeholder="Choose"
|
placeholder="Select user"
|
||||||
value={value}
|
value={value}
|
||||||
autosize={true}
|
autosize={true}
|
||||||
/>
|
/>
|
||||||
@ -78,5 +83,3 @@ class UserPicker extends Component<IProps, any> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withPicker(UserPicker);
|
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export interface IProps {
|
|
||||||
backendSrv: any;
|
|
||||||
handlePicked: (data) => void;
|
|
||||||
value?: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function withPicker(WrappedComponent) {
|
|
||||||
return class WithPicker extends Component<IProps, any> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.toggleLoading = this.toggleLoading.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleLoading(isLoading) {
|
|
||||||
this.setState(prevState => {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
isLoading: isLoading,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <WrappedComponent toggleLoading={this.toggleLoading} isLoading={this.state.isLoading} {...this.props} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -8,7 +8,7 @@ import appEvents from 'app/core/app_events';
|
|||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
import { createStore } from 'app/stores/store';
|
import { createStore } from 'app/stores/store';
|
||||||
import colors from 'app/core/utils/colors';
|
import colors from 'app/core/utils/colors';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
|
|
||||||
export class GrafanaCtrl {
|
export class GrafanaCtrl {
|
||||||
@ -24,6 +24,8 @@ export class GrafanaCtrl {
|
|||||||
backendSrv: BackendSrv,
|
backendSrv: BackendSrv,
|
||||||
datasourceSrv: DatasourceSrv
|
datasourceSrv: DatasourceSrv
|
||||||
) {
|
) {
|
||||||
|
// sets singleston instances for angular services so react components can access them
|
||||||
|
setBackendSrv(backendSrv);
|
||||||
createStore({ backendSrv, datasourceSrv });
|
createStore({ backendSrv, datasourceSrv });
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = function() {
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<div class="dropdown">
|
|
||||||
<gf-form-dropdown model="ctrl.group"
|
|
||||||
get-options="ctrl.debouncedSearchGroups($query)"
|
|
||||||
css-class="gf-size-auto"
|
|
||||||
on-change="ctrl.onChange($option)"
|
|
||||||
</gf-form-dropdown>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
export class TeamPickerCtrl {
|
|
||||||
group: any;
|
|
||||||
teamPicked: any;
|
|
||||||
debouncedSearchGroups: any;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private backendSrv) {
|
|
||||||
this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {
|
|
||||||
leading: true,
|
|
||||||
trailing: false,
|
|
||||||
});
|
|
||||||
this.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.group = { text: 'Choose', value: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
searchGroups(query: string) {
|
|
||||||
return Promise.resolve(
|
|
||||||
this.backendSrv.get('/api/teams/search?perpage=10&page=1&query=' + query).then(result => {
|
|
||||||
return _.map(result.teams, ug => {
|
|
||||||
return { text: ug.name, value: ug };
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(option) {
|
|
||||||
this.teamPicked({ $group: option.value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function teamPicker() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
template: template,
|
|
||||||
controller: TeamPickerCtrl,
|
|
||||||
bindToController: true,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
scope: {
|
|
||||||
teamPicked: '&',
|
|
||||||
},
|
|
||||||
link: function(scope, elem, attrs, ctrl) {
|
|
||||||
scope.$on('team-picker-reset', () => {
|
|
||||||
ctrl.reset();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.directive('teamPicker', teamPicker);
|
|
@ -1,71 +0,0 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<div class="dropdown">
|
|
||||||
<gf-form-dropdown model="ctrl.user"
|
|
||||||
get-options="ctrl.debouncedSearchUsers($query)"
|
|
||||||
css-class="gf-size-auto"
|
|
||||||
on-change="ctrl.onChange($option)"
|
|
||||||
</gf-form-dropdown>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
export class UserPickerCtrl {
|
|
||||||
user: any;
|
|
||||||
debouncedSearchUsers: any;
|
|
||||||
userPicked: any;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private backendSrv) {
|
|
||||||
this.reset();
|
|
||||||
this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {
|
|
||||||
leading: true,
|
|
||||||
trailing: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
searchUsers(query: string) {
|
|
||||||
return Promise.resolve(
|
|
||||||
this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => {
|
|
||||||
return _.map(result.users, user => {
|
|
||||||
return { text: user.login + ' - ' + user.email, value: user };
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(option) {
|
|
||||||
this.userPicked({ $user: option.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.user = { text: 'Choose', value: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
login: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function userPicker() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
template: template,
|
|
||||||
controller: UserPickerCtrl,
|
|
||||||
bindToController: true,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
scope: {
|
|
||||||
userPicked: '&',
|
|
||||||
},
|
|
||||||
link: function(scope, elem, attrs, ctrl) {
|
|
||||||
scope.$on('user-picker-reset', () => {
|
|
||||||
ctrl.reset();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.directive('userPicker', userPicker);
|
|
@ -44,8 +44,6 @@ import { KeybindingSrv } from './services/keybindingSrv';
|
|||||||
import { helpModal } from './components/help/help';
|
import { helpModal } from './components/help/help';
|
||||||
import { JsonExplorer } from './components/json_explorer/json_explorer';
|
import { JsonExplorer } from './components/json_explorer/json_explorer';
|
||||||
import { NavModelSrv, NavModel } from './nav_model_srv';
|
import { NavModelSrv, NavModel } from './nav_model_srv';
|
||||||
import { userPicker } from './components/user_picker';
|
|
||||||
import { teamPicker } from './components/team_picker';
|
|
||||||
import { geminiScrollbar } from './components/scroll/scroll';
|
import { geminiScrollbar } from './components/scroll/scroll';
|
||||||
import { pageScrollbar } from './components/scroll/page_scroll';
|
import { pageScrollbar } from './components/scroll/page_scroll';
|
||||||
import { gfPageDirective } from './components/gf_page';
|
import { gfPageDirective } from './components/gf_page';
|
||||||
@ -83,8 +81,6 @@ export {
|
|||||||
JsonExplorer,
|
JsonExplorer,
|
||||||
NavModelSrv,
|
NavModelSrv,
|
||||||
NavModel,
|
NavModel,
|
||||||
userPicker,
|
|
||||||
teamPicker,
|
|
||||||
geminiScrollbar,
|
geminiScrollbar,
|
||||||
pageScrollbar,
|
pageScrollbar,
|
||||||
gfPageDirective,
|
gfPageDirective,
|
||||||
|
@ -368,3 +368,17 @@ export class BackendSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
coreModule.service('backendSrv', BackendSrv);
|
coreModule.service('backendSrv', BackendSrv);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Code below is to expore the service to react components
|
||||||
|
//
|
||||||
|
|
||||||
|
let singletonInstance: BackendSrv;
|
||||||
|
|
||||||
|
export function setBackendSrv(instance: BackendSrv) {
|
||||||
|
singletonInstance = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackendSrv(): BackendSrv {
|
||||||
|
return singletonInstance;
|
||||||
|
}
|
||||||
|
@ -5,8 +5,6 @@ import './select_org_ctrl';
|
|||||||
import './change_password_ctrl';
|
import './change_password_ctrl';
|
||||||
import './new_org_ctrl';
|
import './new_org_ctrl';
|
||||||
import './user_invite_ctrl';
|
import './user_invite_ctrl';
|
||||||
import './teams_ctrl';
|
|
||||||
import './team_details_ctrl';
|
|
||||||
import './create_team_ctrl';
|
import './create_team_ctrl';
|
||||||
import './org_api_keys_ctrl';
|
import './org_api_keys_ctrl';
|
||||||
import './org_details_ctrl';
|
import './org_details_ctrl';
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
<page-header model="ctrl.navModel"></page-header>
|
|
||||||
|
|
||||||
<div class="page-container page-body">
|
|
||||||
<h3 class="page-sub-heading">Team Details</h3>
|
|
||||||
|
|
||||||
<form name="teamDetailsForm" class="gf-form-group">
|
|
||||||
<div class="gf-form max-width-30">
|
|
||||||
<span class="gf-form-label width-10">Name</span>
|
|
||||||
<input type="text" required ng-model="ctrl.team.name" class="gf-form-input max-width-22">
|
|
||||||
</div>
|
|
||||||
<div class="gf-form max-width-30">
|
|
||||||
<span class="gf-form-label width-10">
|
|
||||||
Email
|
|
||||||
<info-popover mode="right-normal">
|
|
||||||
This is optional and is primarily used for allowing custom team avatars.
|
|
||||||
</info-popover>
|
|
||||||
</span>
|
|
||||||
<input class="gf-form-input max-width-22" type="email" ng-model="ctrl.team.email" placeholder="email@test.com">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-button-row">
|
|
||||||
<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Update</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="gf-form-group">
|
|
||||||
|
|
||||||
<h3 class="page-heading">Team Members</h3>
|
|
||||||
<form name="ctrl.addMemberForm" class="gf-form-group">
|
|
||||||
<div class="gf-form">
|
|
||||||
<span class="gf-form-label width-10">Add member</span>
|
|
||||||
<!--
|
|
||||||
Old picker
|
|
||||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
|
||||||
-->
|
|
||||||
<select-user-picker class="width-7" handlePicked="ctrl.userPicked" backendSrv="ctrl.backendSrv"></select-user-picker>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="filter-table" ng-show="ctrl.teamMembers.length > 0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tr ng-repeat="member in ctrl.teamMembers">
|
|
||||||
<td class="width-4 text-center link-td">
|
|
||||||
<img class="filter-table__avatar" ng-src="{{member.avatarUrl}}"></img>
|
|
||||||
</td>
|
|
||||||
<td>{{member.login}}</td>
|
|
||||||
<td>{{member.email}}</td>
|
|
||||||
<td style="width: 1%">
|
|
||||||
<a ng-click="ctrl.removeTeamMember(member)" class="btn btn-danger btn-mini">
|
|
||||||
<i class="fa fa-remove"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div>
|
|
||||||
<em class="muted" ng-hide="ctrl.teamMembers.length > 0">
|
|
||||||
This team has no members yet.
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-group" ng-if="ctrl.isMappingsEnabled">
|
|
||||||
|
|
||||||
<h3 class="page-heading">Mappings to external groups</h3>
|
|
||||||
<form name="ctrl.addGroupForm" class="gf-form-group">
|
|
||||||
<div class="gf-form">
|
|
||||||
<span class="gf-form-label width-10">Add group</span>
|
|
||||||
<input class="gf-form-input max-width-22" type="text" ng-model="ctrl.newGroupId">
|
|
||||||
</div>
|
|
||||||
<div class="gf-form-button-row">
|
|
||||||
<button type="submit" class="btn btn-success" ng-click="ctrl.addGroup()">Add</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="filter-table" ng-show="ctrl.teamGroups.length > 0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Group</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tr ng-repeat="group in ctrl.teamGroups">
|
|
||||||
<td>{{group.groupId}}</td>
|
|
||||||
<td style="width: 1%">
|
|
||||||
<a ng-click="ctrl.removeGroup(group)" class="btn btn-danger btn-mini">
|
|
||||||
<i class="fa fa-remove"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div>
|
|
||||||
<em class="muted" ng-hide="ctrl.teamGroups.length > 0">
|
|
||||||
This team has no associated groups yet.
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
@ -1,68 +0,0 @@
|
|||||||
<page-header model="ctrl.navModel"></page-header>
|
|
||||||
|
|
||||||
<div class="page-container page-body">
|
|
||||||
<div class="page-action-bar">
|
|
||||||
<label class="gf-form gf-form--grow gf-form--has-input-icon">
|
|
||||||
<input type="text" class="gf-form-input max-width-20" placeholder="Find Team by name" tabindex="1" ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.get()" />
|
|
||||||
<i class="gf-form-input-icon fa fa-search"></i>
|
|
||||||
</label>
|
|
||||||
<div class="page-action-bar__spacer"></div>
|
|
||||||
|
|
||||||
<a class="btn btn-success" href="org/teams/new">
|
|
||||||
<i class="fa fa-plus"></i>
|
|
||||||
Add Team
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-list-table">
|
|
||||||
<table class="filter-table filter-table--hover form-inline" ng-show="ctrl.teams.length > 0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Members</th>
|
|
||||||
<th style="width: 1%"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr ng-repeat="team in ctrl.teams">
|
|
||||||
<td class="width-4 text-center link-td">
|
|
||||||
<a href="org/teams/edit/{{team.id}}">
|
|
||||||
<img class="filter-table__avatar" ng-src="{{team.avatarUrl}}"></img>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="link-td">
|
|
||||||
<a href="org/teams/edit/{{team.id}}">{{team.name}}</a>
|
|
||||||
</td>
|
|
||||||
<td class="link-td">
|
|
||||||
<a href="org/teams/edit/{{team.id}}">{{team.email}}</a>
|
|
||||||
</td>
|
|
||||||
<td class="link-td">
|
|
||||||
<a href="org/teams/edit/{{team.id}}">{{team.memberCount}}</a>
|
|
||||||
</td>
|
|
||||||
<td class="text-right">
|
|
||||||
<a ng-click="ctrl.deleteTeam(team)" class="btn btn-danger btn-small">
|
|
||||||
<i class="fa fa-remove"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-list-paging" ng-if="ctrl.showPaging">
|
|
||||||
<ol>
|
|
||||||
<li ng-repeat="page in ctrl.pages">
|
|
||||||
<button
|
|
||||||
class="btn btn-small"
|
|
||||||
ng-class="{'btn-secondary': page.current, 'btn-inverse': !page.current}"
|
|
||||||
ng-click="ctrl.navigateToPage(page)">{{page.page}}</button>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<em class="muted" ng-hide="ctrl.teams.length > 0">
|
|
||||||
No Teams found.
|
|
||||||
</em>
|
|
||||||
</div>
|
|
@ -1,42 +0,0 @@
|
|||||||
import '../team_details_ctrl';
|
|
||||||
import TeamDetailsCtrl from '../team_details_ctrl';
|
|
||||||
|
|
||||||
describe('TeamDetailsCtrl', () => {
|
|
||||||
var backendSrv = {
|
|
||||||
searchUsers: jest.fn(() => Promise.resolve([])),
|
|
||||||
get: jest.fn(() => Promise.resolve([])),
|
|
||||||
post: jest.fn(() => Promise.resolve([])),
|
|
||||||
};
|
|
||||||
|
|
||||||
//Team id
|
|
||||||
var routeParams = {
|
|
||||||
id: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
var navModelSrv = {
|
|
||||||
getNav: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
var teamDetailsCtrl = new TeamDetailsCtrl({ $broadcast: jest.fn() }, backendSrv, routeParams, navModelSrv);
|
|
||||||
|
|
||||||
describe('when user is chosen to be added to team', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
teamDetailsCtrl = new TeamDetailsCtrl({ $broadcast: jest.fn() }, backendSrv, routeParams, navModelSrv);
|
|
||||||
const userItem = {
|
|
||||||
id: 2,
|
|
||||||
login: 'user2',
|
|
||||||
};
|
|
||||||
teamDetailsCtrl.userPicked(userItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse the result and save to db', () => {
|
|
||||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/teams/1/members');
|
|
||||||
expect(backendSrv.post.mock.calls[0][1].userId).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should refresh the list after saving.', () => {
|
|
||||||
expect(backendSrv.get.mock.calls[0][0]).toBe('/api/teams/1');
|
|
||||||
expect(backendSrv.get.mock.calls[1][0]).toBe('/api/teams/1/members');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,108 +0,0 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
|
|
||||||
export default class TeamDetailsCtrl {
|
|
||||||
team: Team;
|
|
||||||
teamMembers: User[] = [];
|
|
||||||
navModel: any;
|
|
||||||
teamGroups: TeamGroup[] = [];
|
|
||||||
newGroupId: string;
|
|
||||||
isMappingsEnabled: boolean;
|
|
||||||
|
|
||||||
/** @ngInject **/
|
|
||||||
constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) {
|
|
||||||
this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
|
|
||||||
this.userPicked = this.userPicked.bind(this);
|
|
||||||
this.get = this.get.bind(this);
|
|
||||||
this.newGroupId = '';
|
|
||||||
this.isMappingsEnabled = config.buildInfo.isEnterprise;
|
|
||||||
this.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
get() {
|
|
||||||
if (this.$routeParams && this.$routeParams.id) {
|
|
||||||
this.backendSrv.get(`/api/teams/${this.$routeParams.id}`).then(result => {
|
|
||||||
this.team = result;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.backendSrv.get(`/api/teams/${this.$routeParams.id}/members`).then(result => {
|
|
||||||
this.teamMembers = result;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.isMappingsEnabled) {
|
|
||||||
this.backendSrv.get(`/api/teams/${this.$routeParams.id}/groups`).then(result => {
|
|
||||||
this.teamGroups = result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTeamMember(teamMember: TeamMember) {
|
|
||||||
this.$scope.appEvent('confirm-modal', {
|
|
||||||
title: 'Remove Member',
|
|
||||||
text: 'Are you sure you want to remove ' + teamMember.login + ' from this group?',
|
|
||||||
yesText: 'Remove',
|
|
||||||
icon: 'fa-warning',
|
|
||||||
onConfirm: () => {
|
|
||||||
this.removeMemberConfirmed(teamMember);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeMemberConfirmed(teamMember: TeamMember) {
|
|
||||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get);
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (!this.$scope.teamDetailsForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.backendSrv.put('/api/teams/' + this.team.id, {
|
|
||||||
name: this.team.name,
|
|
||||||
email: this.team.email,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
userPicked(user) {
|
|
||||||
this.backendSrv.post(`/api/teams/${this.$routeParams.id}/members`, { userId: user.id }).then(() => {
|
|
||||||
this.$scope.$broadcast('user-picker-reset');
|
|
||||||
this.get();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addGroup() {
|
|
||||||
this.backendSrv.post(`/api/teams/${this.$routeParams.id}/groups`, { groupId: this.newGroupId }).then(() => {
|
|
||||||
this.get();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeGroup(group: TeamGroup) {
|
|
||||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/groups/${group.groupId}`).then(this.get);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TeamGroup {
|
|
||||||
groupId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Team {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
login: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TeamMember {
|
|
||||||
userId: number;
|
|
||||||
name: string;
|
|
||||||
login: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('TeamDetailsCtrl', TeamDetailsCtrl);
|
|
@ -1,66 +0,0 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
|
|
||||||
export class TeamsCtrl {
|
|
||||||
teams: any;
|
|
||||||
pages = [];
|
|
||||||
perPage = 50;
|
|
||||||
page = 1;
|
|
||||||
totalPages: number;
|
|
||||||
showPaging = false;
|
|
||||||
query: any = '';
|
|
||||||
navModel: any;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private backendSrv, navModelSrv) {
|
|
||||||
this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
|
|
||||||
this.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
get() {
|
|
||||||
this.backendSrv
|
|
||||||
.get(`/api/teams/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
|
|
||||||
.then(result => {
|
|
||||||
this.teams = result.teams;
|
|
||||||
this.page = result.page;
|
|
||||||
this.perPage = result.perPage;
|
|
||||||
this.totalPages = Math.ceil(result.totalCount / result.perPage);
|
|
||||||
this.showPaging = this.totalPages > 1;
|
|
||||||
this.pages = [];
|
|
||||||
|
|
||||||
for (var i = 1; i < this.totalPages + 1; i++) {
|
|
||||||
this.pages.push({ page: i, current: i === this.page });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToPage(page) {
|
|
||||||
this.page = page.page;
|
|
||||||
this.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTeam(team) {
|
|
||||||
appEvents.emit('confirm-modal', {
|
|
||||||
title: 'Delete',
|
|
||||||
text: 'Are you sure you want to delete Team ' + team.name + '?',
|
|
||||||
yesText: 'Delete',
|
|
||||||
icon: 'fa-warning',
|
|
||||||
onConfirm: () => {
|
|
||||||
this.deleteTeamConfirmed(team);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTeamConfirmed(team) {
|
|
||||||
this.backendSrv.delete('/api/teams/' + team.id).then(this.get.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
openTeamModal() {
|
|
||||||
appEvents.emit('show-modal', {
|
|
||||||
templateHtml: '<create-team-modal></create-team-modal>',
|
|
||||||
modalClass: 'modal--narrow',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('TeamsCtrl', TeamsCtrl);
|
|
@ -5,6 +5,8 @@ import ServerStats from 'app/containers/ServerStats/ServerStats';
|
|||||||
import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList';
|
import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList';
|
||||||
import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
|
import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
|
||||||
import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
|
import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
|
||||||
|
import TeamPages from 'app/containers/Teams/TeamPages';
|
||||||
|
import TeamList from 'app/containers/Teams/TeamList';
|
||||||
|
|
||||||
/** @ngInject **/
|
/** @ngInject **/
|
||||||
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||||
@ -140,19 +142,23 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
controller: 'OrgApiKeysCtrl',
|
controller: 'OrgApiKeysCtrl',
|
||||||
})
|
})
|
||||||
.when('/org/teams', {
|
.when('/org/teams', {
|
||||||
templateUrl: 'public/app/features/org/partials/teams.html',
|
template: '<react-container />',
|
||||||
controller: 'TeamsCtrl',
|
resolve: {
|
||||||
controllerAs: 'ctrl',
|
roles: () => ['Editor', 'Admin'],
|
||||||
|
component: () => TeamList,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/org/teams/new', {
|
.when('/org/teams/new', {
|
||||||
templateUrl: 'public/app/features/org/partials/create_team.html',
|
templateUrl: 'public/app/features/org/partials/create_team.html',
|
||||||
controller: 'CreateTeamCtrl',
|
controller: 'CreateTeamCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
})
|
})
|
||||||
.when('/org/teams/edit/:id', {
|
.when('/org/teams/edit/:id/:page?', {
|
||||||
templateUrl: 'public/app/features/org/partials/team_details.html',
|
template: '<react-container />',
|
||||||
controller: 'TeamDetailsCtrl',
|
resolve: {
|
||||||
controllerAs: 'ctrl',
|
roles: () => ['Admin'],
|
||||||
|
component: () => TeamPages,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/profile', {
|
.when('/profile', {
|
||||||
templateUrl: 'public/app/features/org/partials/profile.html',
|
templateUrl: 'public/app/features/org/partials/profile.html',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { types } from 'mobx-state-tree';
|
import { types } from 'mobx-state-tree';
|
||||||
|
|
||||||
export const NavItem = types.model('NavItem', {
|
export const NavItem = types.model('NavItem', {
|
||||||
id: types.identifier(types.string),
|
id: types.identifier(types.string),
|
||||||
@ -8,6 +8,7 @@ export const NavItem = types.model('NavItem', {
|
|||||||
icon: types.optional(types.string, ''),
|
icon: types.optional(types.string, ''),
|
||||||
img: types.optional(types.string, ''),
|
img: types.optional(types.string, ''),
|
||||||
active: types.optional(types.boolean, false),
|
active: types.optional(types.boolean, false),
|
||||||
|
hideFromTabs: types.optional(types.boolean, false),
|
||||||
breadcrumbs: types.optional(types.array(types.late(() => Breadcrumb)), []),
|
breadcrumbs: types.optional(types.array(types.late(() => Breadcrumb)), []),
|
||||||
children: types.optional(types.array(types.late(() => NavItem)), []),
|
children: types.optional(types.array(types.late(() => NavItem)), []),
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { types, getEnv } from 'mobx-state-tree';
|
import { types, getEnv } from 'mobx-state-tree';
|
||||||
import { NavItem } from './NavItem';
|
import { NavItem } from './NavItem';
|
||||||
|
import { ITeam } from '../TeamsStore/TeamsStore';
|
||||||
|
|
||||||
export const NavStore = types
|
export const NavStore = types
|
||||||
.model('NavStore', {
|
.model('NavStore', {
|
||||||
@ -115,4 +116,43 @@ export const NavStore = types
|
|||||||
|
|
||||||
self.main = NavItem.create(main);
|
self.main = NavItem.create(main);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
initTeamPage(team: ITeam, tab: string, isSyncEnabled: boolean) {
|
||||||
|
let main = {
|
||||||
|
img: team.avatarUrl,
|
||||||
|
id: 'team-' + team.id,
|
||||||
|
subTitle: 'Manage members & settings',
|
||||||
|
url: '',
|
||||||
|
text: team.name,
|
||||||
|
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
active: tab === 'members',
|
||||||
|
icon: 'gicon gicon-team',
|
||||||
|
id: 'team-members',
|
||||||
|
text: 'Members',
|
||||||
|
url: `org/teams/edit/${team.id}/members`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: tab === 'settings',
|
||||||
|
icon: 'fa fa-fw fa-sliders',
|
||||||
|
id: 'team-settings',
|
||||||
|
text: 'Settings',
|
||||||
|
url: `org/teams/edit/${team.id}/settings`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSyncEnabled) {
|
||||||
|
main.children.splice(1, 0, {
|
||||||
|
active: tab === 'groupsync',
|
||||||
|
icon: 'fa fa-fw fa-refresh',
|
||||||
|
id: 'team-settings',
|
||||||
|
text: 'External group sync',
|
||||||
|
url: `org/teams/edit/${team.id}/groupsync`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.main = NavItem.create(main);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -6,6 +6,7 @@ import { AlertListStore } from './../AlertListStore/AlertListStore';
|
|||||||
import { ViewStore } from './../ViewStore/ViewStore';
|
import { ViewStore } from './../ViewStore/ViewStore';
|
||||||
import { FolderStore } from './../FolderStore/FolderStore';
|
import { FolderStore } from './../FolderStore/FolderStore';
|
||||||
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
|
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
|
||||||
|
import { TeamsStore } from './../TeamsStore/TeamsStore';
|
||||||
|
|
||||||
export const RootStore = types.model({
|
export const RootStore = types.model({
|
||||||
search: types.optional(SearchStore, {
|
search: types.optional(SearchStore, {
|
||||||
@ -28,6 +29,9 @@ export const RootStore = types.model({
|
|||||||
routeParams: {},
|
routeParams: {},
|
||||||
}),
|
}),
|
||||||
folder: types.optional(FolderStore, {}),
|
folder: types.optional(FolderStore, {}),
|
||||||
|
teams: types.optional(TeamsStore, {
|
||||||
|
map: {},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type IRootStoreType = typeof RootStore.Type;
|
type IRootStoreType = typeof RootStore.Type;
|
||||||
|
156
public/app/stores/TeamsStore/TeamsStore.ts
Normal file
156
public/app/stores/TeamsStore/TeamsStore.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { types, getEnv, flow } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
export const TeamMember = types.model('TeamMember', {
|
||||||
|
userId: types.identifier(types.number),
|
||||||
|
teamId: types.number,
|
||||||
|
avatarUrl: types.string,
|
||||||
|
email: types.string,
|
||||||
|
login: types.string,
|
||||||
|
});
|
||||||
|
|
||||||
|
type TeamMemberType = typeof TeamMember.Type;
|
||||||
|
export interface ITeamMember extends TeamMemberType {}
|
||||||
|
|
||||||
|
export const TeamGroup = types.model('TeamGroup', {
|
||||||
|
groupId: types.identifier(types.string),
|
||||||
|
teamId: types.number,
|
||||||
|
});
|
||||||
|
|
||||||
|
type TeamGroupType = typeof TeamGroup.Type;
|
||||||
|
export interface ITeamGroup extends TeamGroupType {}
|
||||||
|
|
||||||
|
export const Team = types
|
||||||
|
.model('Team', {
|
||||||
|
id: types.identifier(types.number),
|
||||||
|
name: types.string,
|
||||||
|
avatarUrl: types.string,
|
||||||
|
email: types.string,
|
||||||
|
memberCount: types.number,
|
||||||
|
search: types.optional(types.string, ''),
|
||||||
|
members: types.optional(types.map(TeamMember), {}),
|
||||||
|
groups: types.optional(types.map(TeamGroup), {}),
|
||||||
|
})
|
||||||
|
.views(self => ({
|
||||||
|
get filteredMembers() {
|
||||||
|
let members = this.members.values();
|
||||||
|
let regex = new RegExp(self.search, 'i');
|
||||||
|
return members.filter(member => {
|
||||||
|
return regex.test(member.login) || regex.test(member.email);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.actions(self => ({
|
||||||
|
setName(name: string) {
|
||||||
|
self.name = name;
|
||||||
|
},
|
||||||
|
|
||||||
|
setEmail(email: string) {
|
||||||
|
self.email = email;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSearchQuery(query: string) {
|
||||||
|
self.search = query;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: flow(function* load() {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
|
||||||
|
yield backendSrv.put(`/api/teams/${self.id}`, {
|
||||||
|
name: self.name,
|
||||||
|
email: self.email,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
loadMembers: flow(function* load() {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
const rsp = yield backendSrv.get(`/api/teams/${self.id}/members`);
|
||||||
|
self.members.clear();
|
||||||
|
|
||||||
|
for (let member of rsp) {
|
||||||
|
self.members.set(member.userId.toString(), TeamMember.create(member));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeMember: flow(function* load(member: ITeamMember) {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
yield backendSrv.delete(`/api/teams/${self.id}/members/${member.userId}`);
|
||||||
|
// remove from store map
|
||||||
|
self.members.delete(member.userId.toString());
|
||||||
|
}),
|
||||||
|
|
||||||
|
addMember: flow(function* load(userId: number) {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
yield backendSrv.post(`/api/teams/${self.id}/members`, { userId: userId });
|
||||||
|
}),
|
||||||
|
|
||||||
|
loadGroups: flow(function* load() {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
const rsp = yield backendSrv.get(`/api/teams/${self.id}/groups`);
|
||||||
|
self.groups.clear();
|
||||||
|
|
||||||
|
for (let group of rsp) {
|
||||||
|
self.groups.set(group.groupId, TeamGroup.create(group));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
addGroup: flow(function* load(groupId: string) {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
yield backendSrv.post(`/api/teams/${self.id}/groups`, { groupId: groupId });
|
||||||
|
self.groups.set(
|
||||||
|
groupId,
|
||||||
|
TeamGroup.create({
|
||||||
|
teamId: self.id,
|
||||||
|
groupId: groupId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeGroup: flow(function* load(groupId: string) {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
yield backendSrv.delete(`/api/teams/${self.id}/groups/${groupId}`);
|
||||||
|
self.groups.delete(groupId);
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
type TeamType = typeof Team.Type;
|
||||||
|
export interface ITeam extends TeamType {}
|
||||||
|
|
||||||
|
export const TeamsStore = types
|
||||||
|
.model('TeamsStore', {
|
||||||
|
map: types.map(Team),
|
||||||
|
search: types.optional(types.string, ''),
|
||||||
|
})
|
||||||
|
.views(self => ({
|
||||||
|
get filteredTeams() {
|
||||||
|
let teams = this.map.values();
|
||||||
|
let regex = new RegExp(self.search, 'i');
|
||||||
|
return teams.filter(team => {
|
||||||
|
return regex.test(team.name);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.actions(self => ({
|
||||||
|
loadTeams: flow(function* load() {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
const rsp = yield backendSrv.get('/api/teams/search/', { perpage: 50, page: 1 });
|
||||||
|
self.map.clear();
|
||||||
|
|
||||||
|
for (let team of rsp.teams) {
|
||||||
|
self.map.set(team.id.toString(), Team.create(team));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
setSearchQuery(query: string) {
|
||||||
|
self.search = query;
|
||||||
|
},
|
||||||
|
|
||||||
|
loadById: flow(function* load(id: string) {
|
||||||
|
if (self.map.has(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
const team = yield backendSrv.get(`/api/teams/${id}`);
|
||||||
|
self.map.set(id, Team.create(team));
|
||||||
|
}),
|
||||||
|
}));
|
@ -403,9 +403,9 @@ select.gf-form-input ~ .gf-form-help-icon {
|
|||||||
|
|
||||||
.cta-form {
|
.cta-form {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 1rem;
|
padding: 1.5rem;
|
||||||
background-color: $empty-list-cta-bg;
|
background-color: $empty-list-cta-bg;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 2rem;
|
||||||
border-top: 3px solid $green;
|
border-top: 3px solid $green;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
declare var global: NodeJS.Global;
|
declare var global: NodeJS.Global;
|
||||||
|
|
||||||
(<any>global).requestAnimationFrame = (callback) => {
|
(<any>global).requestAnimationFrame = callback => {
|
||||||
setTimeout(callback, 0);
|
setTimeout(callback, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(<any>Promise.prototype).finally = function(onFinally) {
|
||||||
|
return this.then(
|
||||||
|
/* onFulfilled */
|
||||||
|
res => Promise.resolve(onFinally()).then(() => res),
|
||||||
|
/* onRejected */
|
||||||
|
err =>
|
||||||
|
Promise.resolve(onFinally()).then(() => {
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user