TeamMembers.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import React, { PureComponent } from 'react';
  2. import { connect } from 'react-redux';
  3. import SlideDown from 'app/core/components/Animations/SlideDown';
  4. import { UserPicker } from 'app/core/components/Select/UserPicker';
  5. import { DeleteButton, Select, SelectOptionItem } from '@grafana/ui';
  6. import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
  7. import { TeamMember, User, teamsPermissionLevels, TeamPermissionLevel, OrgRole } from 'app/types';
  8. import {
  9. loadTeamMembers,
  10. addTeamMember,
  11. removeTeamMember,
  12. setSearchMemberQuery,
  13. updateTeamMember,
  14. } from './state/actions';
  15. import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
  16. import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
  17. import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
  18. import { config } from 'app/core/config';
  19. import { contextSrv } from 'app/core/services/context_srv';
  20. export interface Props {
  21. members: TeamMember[];
  22. searchMemberQuery: string;
  23. loadTeamMembers: typeof loadTeamMembers;
  24. addTeamMember: typeof addTeamMember;
  25. removeTeamMember: typeof removeTeamMember;
  26. setSearchMemberQuery: typeof setSearchMemberQuery;
  27. updateTeamMember: typeof updateTeamMember;
  28. syncEnabled: boolean;
  29. editorsCanAdmin?: boolean;
  30. }
  31. export interface State {
  32. isAdding: boolean;
  33. newTeamMember?: User;
  34. }
  35. export class TeamMembers extends PureComponent<Props, State> {
  36. constructor(props) {
  37. super(props);
  38. this.state = { isAdding: false, newTeamMember: null };
  39. this.renderPermissionsSelect = this.renderPermissionsSelect.bind(this);
  40. }
  41. componentDidMount() {
  42. this.props.loadTeamMembers();
  43. }
  44. onSearchQueryChange = (value: string) => {
  45. this.props.setSearchMemberQuery(value);
  46. };
  47. onRemoveMember(member: TeamMember) {
  48. this.props.removeTeamMember(member.userId);
  49. }
  50. onToggleAdding = () => {
  51. this.setState({ isAdding: !this.state.isAdding });
  52. };
  53. onUserSelected = (user: User) => {
  54. this.setState({ newTeamMember: user });
  55. };
  56. onAddUserToTeam = async () => {
  57. this.props.addTeamMember(this.state.newTeamMember.id);
  58. this.setState({ newTeamMember: null });
  59. };
  60. renderLabels(labels: string[]) {
  61. if (!labels) {
  62. return <td />;
  63. }
  64. return (
  65. <td>
  66. {labels.map(label => (
  67. <TagBadge key={label} label={label} removeIcon={false} count={0} onClick={() => {}} />
  68. ))}
  69. </td>
  70. );
  71. }
  72. onPermissionChange = (item: SelectOptionItem, member: TeamMember) => {
  73. const permission = item.value;
  74. const updatedTeamMember = { ...member, permission };
  75. this.props.updateTeamMember(updatedTeamMember);
  76. };
  77. renderPermissionsSelect(member: TeamMember) {
  78. const { members, editorsCanAdmin } = this.props;
  79. const userInMembers = members.find(m => m.userId === contextSrv.user.id);
  80. const isUserTeamAdmin =
  81. contextSrv.isGrafanaAdmin || contextSrv.hasRole(OrgRole.Admin)
  82. ? true
  83. : userInMembers && userInMembers.permission === TeamPermissionLevel.Admin;
  84. const value = teamsPermissionLevels.find(dp => dp.value === member.permission);
  85. return (
  86. <WithFeatureToggle featureToggle={editorsCanAdmin}>
  87. <td>
  88. <div className="gf-form">
  89. {isUserTeamAdmin && (
  90. <Select
  91. isSearchable={false}
  92. options={teamsPermissionLevels}
  93. onChange={item => this.onPermissionChange(item, member)}
  94. className="gf-form-select-box__control--menu-right"
  95. value={value}
  96. />
  97. )}
  98. {!isUserTeamAdmin && <span>{value.label}</span>}
  99. </div>
  100. </td>
  101. </WithFeatureToggle>
  102. );
  103. }
  104. renderMember(member: TeamMember, syncEnabled: boolean) {
  105. return (
  106. <tr key={member.userId}>
  107. <td className="width-4 text-center">
  108. <img className="filter-table__avatar" src={member.avatarUrl} />
  109. </td>
  110. <td>{member.login}</td>
  111. <td>{member.email}</td>
  112. {this.renderPermissionsSelect(member)}
  113. {syncEnabled && this.renderLabels(member.labels)}
  114. <td className="text-right">
  115. <DeleteButton onConfirm={() => this.onRemoveMember(member)} />
  116. </td>
  117. </tr>
  118. );
  119. }
  120. render() {
  121. const { isAdding } = this.state;
  122. const { searchMemberQuery, members, syncEnabled, editorsCanAdmin } = this.props;
  123. return (
  124. <div>
  125. <div className="page-action-bar">
  126. <div className="gf-form gf-form--grow">
  127. <FilterInput
  128. labelClassName="gf-form--has-input-icon gf-form--grow"
  129. inputClassName="gf-form-input"
  130. placeholder="Search members"
  131. value={searchMemberQuery}
  132. onChange={this.onSearchQueryChange}
  133. />
  134. </div>
  135. <div className="page-action-bar__spacer" />
  136. <button className="btn btn-primary pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
  137. Add member
  138. </button>
  139. </div>
  140. <SlideDown in={isAdding}>
  141. <div className="cta-form">
  142. <button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
  143. <i className="fa fa-close" />
  144. </button>
  145. <h5>Add team member</h5>
  146. <div className="gf-form-inline">
  147. <UserPicker onSelected={this.onUserSelected} className="min-width-30" />
  148. {this.state.newTeamMember && (
  149. <button className="btn btn-primary gf-form-btn" type="submit" onClick={this.onAddUserToTeam}>
  150. Add to team
  151. </button>
  152. )}
  153. </div>
  154. </div>
  155. </SlideDown>
  156. <div className="admin-list-table">
  157. <table className="filter-table filter-table--hover form-inline">
  158. <thead>
  159. <tr>
  160. <th />
  161. <th>Name</th>
  162. <th>Email</th>
  163. <WithFeatureToggle featureToggle={editorsCanAdmin}>
  164. <th>Permission</th>
  165. </WithFeatureToggle>
  166. {syncEnabled && <th />}
  167. <th style={{ width: '1%' }} />
  168. </tr>
  169. </thead>
  170. <tbody>{members && members.map(member => this.renderMember(member, syncEnabled))}</tbody>
  171. </table>
  172. </div>
  173. </div>
  174. );
  175. }
  176. }
  177. function mapStateToProps(state) {
  178. return {
  179. members: getTeamMembers(state.team),
  180. searchMemberQuery: getSearchMemberQuery(state.team),
  181. editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
  182. };
  183. }
  184. const mapDispatchToProps = {
  185. loadTeamMembers,
  186. addTeamMember,
  187. removeTeamMember,
  188. setSearchMemberQuery,
  189. updateTeamMember,
  190. };
  191. export default connect(
  192. mapStateToProps,
  193. mapDispatchToProps
  194. )(TeamMembers);