Commit b2833daf by Marcus Efraimsson Committed by GitHub

Merge pull request #13285 from marefr/team_member_ext

Team member labels
parents dd0b1d84 da68b858
......@@ -4,6 +4,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
......@@ -17,6 +18,11 @@ func GetTeamMembers(c *m.ReqContext) Response {
for _, member := range query.Result {
member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
member.Labels = []string{}
if setting.IsEnterprise && setting.LdapEnabled && member.External {
member.Labels = append(member.Labels, "LDAP")
}
}
return JSON(200, query.Result)
......
......@@ -16,6 +16,7 @@ type TeamMember struct {
OrgId int64
TeamId int64
UserId int64
External bool
Created time.Time
Updated time.Time
......@@ -28,6 +29,7 @@ type AddTeamMemberCommand struct {
UserId int64 `json:"userId" binding:"Required"`
OrgId int64 `json:"-"`
TeamId int64 `json:"-"`
External bool `json:"-"`
}
type RemoveTeamMemberCommand struct {
......@@ -43,6 +45,7 @@ type GetTeamMembersQuery struct {
OrgId int64
TeamId int64
UserId int64
External bool
Result []*TeamMemberDTO
}
......@@ -53,7 +56,9 @@ type TeamMemberDTO struct {
OrgId int64 `json:"orgId"`
TeamId int64 `json:"teamId"`
UserId int64 `json:"userId"`
External bool `json:"-"`
Email string `json:"email"`
Login string `json:"login"`
AvatarUrl string `json:"avatarUrl"`
Labels []string `json:"labels"`
}
......@@ -51,4 +51,7 @@ func addTeamMigrations(mg *Migrator) {
Name: "email", Type: DB_NVarchar, Nullable: true, Length: 190,
}))
mg.AddMigration("Add column external to team_member table", NewAddColumnMigration(teamMemberV1, &Column{
Name: "external", Type: DB_Bool, Nullable: true,
}))
}
......@@ -243,6 +243,7 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
OrgId: cmd.OrgId,
TeamId: cmd.TeamId,
UserId: cmd.UserId,
External: cmd.External,
Created: time.Now(),
Updated: time.Now(),
}
......@@ -289,7 +290,10 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
if query.UserId != 0 {
sess.Where("team_member.user_id=?", query.UserId)
}
sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login")
if query.External {
sess.Where("team_member.external=?", dialect.BooleanStr(true))
}
sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external")
sess.Asc("user.login", "user.email")
err := sess.Find(&query.Result)
......
......@@ -50,13 +50,29 @@ func TestTeamCommandsAndQueries(t *testing.T) {
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]})
So(err, ShouldBeNil)
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[1], External: true})
So(err, ShouldBeNil)
q1 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id}
err = GetTeamMembers(q1)
So(err, ShouldBeNil)
So(q1.Result, ShouldHaveLength, 2)
So(q1.Result[0].TeamId, ShouldEqual, team1.Id)
So(q1.Result[0].Login, ShouldEqual, "loginuser0")
So(q1.Result[0].OrgId, ShouldEqual, testOrgId)
So(q1.Result[1].TeamId, ShouldEqual, team1.Id)
So(q1.Result[1].Login, ShouldEqual, "loginuser1")
So(q1.Result[1].OrgId, ShouldEqual, testOrgId)
So(q1.Result[1].External, ShouldEqual, true)
q2 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true}
err = GetTeamMembers(q2)
So(err, ShouldBeNil)
So(q2.Result, ShouldHaveLength, 1)
So(q2.Result[0].TeamId, ShouldEqual, team1.Id)
So(q2.Result[0].Login, ShouldEqual, "loginuser1")
So(q2.Result[0].OrgId, ShouldEqual, testOrgId)
So(q2.Result[0].External, ShouldEqual, true)
})
Convey("Should be able to search for teams", func() {
......
......@@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => {
loadTeamMembers: jest.fn(),
addTeamMember: jest.fn(),
removeTeamMember: jest.fn(),
syncEnabled: false,
};
Object.assign(props, propOverrides);
......@@ -39,6 +40,15 @@ describe('Render', () => {
expect(wrapper).toMatchSnapshot();
});
it('should render team members when sync enabled', () => {
const { wrapper } = setup({
members: getMockTeamMembers(5),
syncEnabled: true,
});
expect(wrapper).toMatchSnapshot();
});
});
describe('Functions', () => {
......
......@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember } from '../../types';
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
......@@ -14,6 +15,7 @@ export interface Props {
addTeamMember: typeof addTeamMember;
removeTeamMember: typeof removeTeamMember;
setSearchMemberQuery: typeof setSearchMemberQuery;
syncEnabled: boolean;
}
export interface State {
......@@ -52,7 +54,19 @@ export class TeamMembers extends PureComponent<Props, State> {
this.setState({ newTeamMember: null });
};
renderMember(member: TeamMember) {
renderLabels(labels: string[]) {
if (!labels) {
return <td />;
}
return (
<td>
{labels.map(label => <TagBadge key={label} label={label} removeIcon={false} count={0} onClick={() => {}} />)}
</td>
);
}
renderMember(member: TeamMember, syncEnabled: boolean) {
return (
<tr key={member.userId}>
<td className="width-4 text-center">
......@@ -60,6 +74,7 @@ export class TeamMembers extends PureComponent<Props, State> {
</td>
<td>{member.login}</td>
<td>{member.email}</td>
{syncEnabled ? this.renderLabels(member.labels) : ''}
<td className="text-right">
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
</td>
......@@ -69,7 +84,7 @@ export class TeamMembers extends PureComponent<Props, State> {
render() {
const { newTeamMember, isAdding } = this.state;
const { searchMemberQuery, members } = this.props;
const { searchMemberQuery, members, syncEnabled } = this.props;
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
return (
......@@ -120,10 +135,11 @@ export class TeamMembers extends PureComponent<Props, State> {
<th />
<th>Name</th>
<th>Email</th>
{syncEnabled ? <th /> : ''}
<th style={{ width: '1%' }} />
</tr>
</thead>
<tbody>{members && members.map(member => this.renderMember(member))}</tbody>
<tbody>{members && members.map(member => this.renderMember(member, syncEnabled))}</tbody>
</table>
</div>
</div>
......
......@@ -63,7 +63,7 @@ export class TeamPages extends PureComponent<Props, State> {
switch (currentPage) {
case PageTypes.Members:
return <TeamMembers />;
return <TeamMembers syncEnabled={isSyncEnabled} />;
case PageTypes.Settings:
return <TeamSettings />;
......
......@@ -35,6 +35,7 @@ export const getMockTeamMembers = (amount: number): TeamMember[] => {
avatarUrl: 'some/url/',
email: 'test@test.com',
login: `testUser-${i}`,
labels: ['label 1', 'label 2'],
});
}
......@@ -48,6 +49,7 @@ export const getMockTeamMember = (): TeamMember => {
avatarUrl: 'some/url/',
email: 'test@test.com',
login: 'testUser',
labels: [],
};
};
......
......@@ -315,3 +315,305 @@ exports[`Render should render team members 1`] = `
</div>
</div>
`;
exports[`Render should render team members when sync enabled 1`] = `
<div>
<div
className="page-action-bar"
>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form--has-input-icon gf-form--grow"
>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Search members"
type="text"
value=""
/>
<i
className="gf-form-input-icon fa fa-search"
/>
</label>
</div>
<div
className="page-action-bar__spacer"
/>
<button
className="btn btn-success pull-right"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-plus"
/>
Add a member
</button>
</div>
<Component
in={false}
>
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
onClick={[Function]}
>
<i
className="fa fa-close"
/>
</button>
<h5>
Add Team Member
</h5>
<div
className="gf-form-inline"
>
<UserPicker
className="width-30"
onSelected={[Function]}
value={null}
/>
</div>
</div>
</Component>
<div
className="admin-list-table"
>
<table
className="filter-table filter-table--hover form-inline"
>
<thead>
<tr>
<th />
<th>
Name
</th>
<th>
Email
</th>
<th />
<th
style={
Object {
"width": "1%",
}
}
/>
</tr>
</thead>
<tbody>
<tr
key="1"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-1
</td>
<td>
test@test.com
</td>
<td>
<TagBadge
count={0}
key="label 1"
label="label 1"
onClick={[Function]}
removeIcon={false}
/>
<TagBadge
count={0}
key="label 2"
label="label 2"
onClick={[Function]}
removeIcon={false}
/>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="2"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-2
</td>
<td>
test@test.com
</td>
<td>
<TagBadge
count={0}
key="label 1"
label="label 1"
onClick={[Function]}
removeIcon={false}
/>
<TagBadge
count={0}
key="label 2"
label="label 2"
onClick={[Function]}
removeIcon={false}
/>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="3"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-3
</td>
<td>
test@test.com
</td>
<td>
<TagBadge
count={0}
key="label 1"
label="label 1"
onClick={[Function]}
removeIcon={false}
/>
<TagBadge
count={0}
key="label 2"
label="label 2"
onClick={[Function]}
removeIcon={false}
/>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="4"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-4
</td>
<td>
test@test.com
</td>
<td>
<TagBadge
count={0}
key="label 1"
label="label 1"
onClick={[Function]}
removeIcon={false}
/>
<TagBadge
count={0}
key="label 2"
label="label 2"
onClick={[Function]}
removeIcon={false}
/>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="5"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-5
</td>
<td>
test@test.com
</td>
<td>
<TagBadge
count={0}
key="label 1"
label="label 1"
onClick={[Function]}
removeIcon={false}
/>
<TagBadge
count={0}
key="label 2"
label="label 2"
onClick={[Function]}
removeIcon={false}
/>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
`;
......@@ -29,7 +29,9 @@ exports[`Render should render member page if team not empty 1`] = `
<div
className="page-container page-body"
>
<Connect(TeamMembers) />
<Connect(TeamMembers)
syncEnabled={true}
/>
</div>
</div>
`;
......
......@@ -12,6 +12,7 @@ export interface TeamMember {
avatarUrl: string;
email: string;
login: string;
labels: string[];
}
export interface TeamGroup {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment