mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13285 from marefr/team_member_ext
Team member labels
This commit is contained in:
commit
b2833daf32
@ -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)
|
||||
|
@ -12,10 +12,11 @@ var (
|
||||
|
||||
// TeamMember model
|
||||
type TeamMember struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
TeamId int64
|
||||
UserId int64
|
||||
Id int64
|
||||
OrgId int64
|
||||
TeamId int64
|
||||
UserId int64
|
||||
External bool
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
@ -25,9 +26,10 @@ type TeamMember struct {
|
||||
// COMMANDS
|
||||
|
||||
type AddTeamMemberCommand struct {
|
||||
UserId int64 `json:"userId" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
TeamId int64 `json:"-"`
|
||||
UserId int64 `json:"userId" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
TeamId int64 `json:"-"`
|
||||
External bool `json:"-"`
|
||||
}
|
||||
|
||||
type RemoveTeamMemberCommand struct {
|
||||
@ -40,20 +42,23 @@ type RemoveTeamMemberCommand struct {
|
||||
// QUERIES
|
||||
|
||||
type GetTeamMembersQuery struct {
|
||||
OrgId int64
|
||||
TeamId int64
|
||||
UserId int64
|
||||
Result []*TeamMemberDTO
|
||||
OrgId int64
|
||||
TeamId int64
|
||||
UserId int64
|
||||
External bool
|
||||
Result []*TeamMemberDTO
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Projections and DTOs
|
||||
|
||||
type TeamMemberDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
TeamId int64 `json:"teamId"`
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
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,
|
||||
}))
|
||||
}
|
||||
|
@ -240,11 +240,12 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
|
||||
}
|
||||
|
||||
entity := m.TeamMember{
|
||||
OrgId: cmd.OrgId,
|
||||
TeamId: cmd.TeamId,
|
||||
UserId: cmd.UserId,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
OrgId: cmd.OrgId,
|
||||
TeamId: cmd.TeamId,
|
||||
UserId: cmd.UserId,
|
||||
External: cmd.External,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
_, err := sess.Insert(&entity)
|
||||
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user