Explorar o código

Merge pull request #13285 from marefr/team_member_ext

Team member labels
Marcus Efraimsson %!s(int64=7) %!d(string=hai) anos
pai
achega
b2833daf32

+ 6 - 0
pkg/api/team_members.go

@@ -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)

+ 22 - 17
pkg/models/team_member.go

@@ -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"`
 }

+ 3 - 0
pkg/services/sqlstore/migrations/team_mig.go

@@ -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,
+	}))
 }

+ 10 - 6
pkg/services/sqlstore/team.go

@@ -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)

+ 16 - 0
pkg/services/sqlstore/team_test.go

@@ -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() {

+ 10 - 0
public/app/features/teams/TeamMembers.test.tsx

@@ -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', () => {

+ 19 - 3
public/app/features/teams/TeamMembers.tsx

@@ -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>

+ 1 - 1
public/app/features/teams/TeamPages.tsx

@@ -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 />;

+ 2 - 0
public/app/features/teams/__mocks__/teamMocks.ts

@@ -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: [],
   };
 };
 

+ 302 - 0
public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap

@@ -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>
+`;

+ 3 - 1
public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap

@@ -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>
 `;

+ 1 - 0
public/app/types/teams.ts

@@ -12,6 +12,7 @@ export interface TeamMember {
   avatarUrl: string;
   email: string;
   login: string;
+  labels: string[];
 }
 
 export interface TeamGroup {