allTeamsRow.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {fetchOrganizationDetails} from 'sentry/actionCreators/organizations';
  5. import {joinTeam, leaveTeam} from 'sentry/actionCreators/teams';
  6. import {Client} from 'sentry/api';
  7. import {Button} from 'sentry/components/button';
  8. import IdBadge from 'sentry/components/idBadge';
  9. import Link from 'sentry/components/links/link';
  10. import {PanelItem} from 'sentry/components/panels';
  11. import {t, tct, tn} from 'sentry/locale';
  12. import TeamStore from 'sentry/stores/teamStore';
  13. import space from 'sentry/styles/space';
  14. import {Organization, Team} from 'sentry/types';
  15. import withApi from 'sentry/utils/withApi';
  16. type Props = {
  17. api: Client;
  18. openMembership: boolean;
  19. organization: Organization;
  20. team: Team;
  21. };
  22. type State = {
  23. error: boolean;
  24. loading: boolean;
  25. };
  26. class AllTeamsRow extends Component<Props, State> {
  27. state: State = {
  28. loading: false,
  29. error: false,
  30. };
  31. reloadProjects() {
  32. const {api, organization} = this.props;
  33. // After a change in teams has happened, refresh the project store
  34. fetchOrganizationDetails(api, organization.slug, {
  35. loadProjects: true,
  36. });
  37. }
  38. handleRequestAccess = () => {
  39. const {team} = this.props;
  40. try {
  41. this.joinTeam({
  42. successMessage: tct('You have requested access to [team]', {
  43. team: `#${team.slug}`,
  44. }),
  45. errorMessage: tct('Unable to request access to [team]', {
  46. team: `#${team.slug}`,
  47. }),
  48. });
  49. // Update team so that `isPending` is true
  50. TeamStore.onUpdateSuccess(team.slug, {
  51. ...team,
  52. isPending: true,
  53. });
  54. } catch (_err) {
  55. // No need to do anything
  56. }
  57. };
  58. handleJoinTeam = async () => {
  59. const {team} = this.props;
  60. await this.joinTeam({
  61. successMessage: tct('You have joined [team]', {
  62. team: `#${team.slug}`,
  63. }),
  64. errorMessage: tct('Unable to join [team]', {
  65. team: `#${team.slug}`,
  66. }),
  67. });
  68. this.reloadProjects();
  69. };
  70. joinTeam = ({
  71. successMessage,
  72. errorMessage,
  73. }: {
  74. errorMessage: React.ReactNode;
  75. successMessage: React.ReactNode;
  76. }) => {
  77. const {api, organization, team} = this.props;
  78. this.setState({
  79. loading: true,
  80. });
  81. return new Promise<void>((resolve, reject) =>
  82. joinTeam(
  83. api,
  84. {
  85. orgId: organization.slug,
  86. teamId: team.slug,
  87. },
  88. {
  89. success: () => {
  90. this.setState({
  91. loading: false,
  92. error: false,
  93. });
  94. addSuccessMessage(successMessage);
  95. resolve();
  96. },
  97. error: () => {
  98. this.setState({
  99. loading: false,
  100. error: true,
  101. });
  102. addErrorMessage(errorMessage);
  103. reject(new Error('Unable to join team'));
  104. },
  105. }
  106. )
  107. );
  108. };
  109. handleLeaveTeam = () => {
  110. const {api, organization, team} = this.props;
  111. this.setState({
  112. loading: true,
  113. });
  114. leaveTeam(
  115. api,
  116. {
  117. orgId: organization.slug,
  118. teamId: team.slug,
  119. },
  120. {
  121. success: () => {
  122. this.setState({
  123. loading: false,
  124. error: false,
  125. });
  126. addSuccessMessage(
  127. tct('You have left [team]', {
  128. team: `#${team.slug}`,
  129. })
  130. );
  131. // Reload ProjectsStore
  132. this.reloadProjects();
  133. },
  134. error: () => {
  135. this.setState({
  136. loading: false,
  137. error: true,
  138. });
  139. addErrorMessage(
  140. tct('Unable to leave [team]', {
  141. team: `#${team.slug}`,
  142. })
  143. );
  144. },
  145. }
  146. );
  147. };
  148. getTeamRoleName = () => {
  149. const {organization, team} = this.props;
  150. if (!organization.features.includes('team-roles') || !team.teamRole) {
  151. return null;
  152. }
  153. const {teamRoleList} = organization;
  154. const roleName = teamRoleList.find(r => r.id === team.teamRole)?.name;
  155. return roleName;
  156. };
  157. render() {
  158. const {team, openMembership, organization} = this.props;
  159. const urlPrefix = `/settings/${organization.slug}/teams/`;
  160. const buttonHelpText = team.flags['idp:provisioned']
  161. ? t(
  162. "Membership to this team is managed through your organization's identity provider."
  163. )
  164. : undefined;
  165. const display = (
  166. <IdBadge
  167. team={team}
  168. avatarSize={36}
  169. description={tn('%s Member', '%s Members', team.memberCount)}
  170. />
  171. );
  172. // You can only view team details if you have access to team -- this should account
  173. // for your role + org open membership
  174. const canViewTeam = team.hasAccess;
  175. const idpProvisioned = team.flags['idp:provisioned'];
  176. return (
  177. <TeamPanelItem>
  178. <div>
  179. {canViewTeam ? (
  180. <TeamLink data-test-id="team-link" to={`${urlPrefix}${team.slug}/`}>
  181. {display}
  182. </TeamLink>
  183. ) : (
  184. display
  185. )}
  186. </div>
  187. <div>{this.getTeamRoleName()}</div>
  188. <div>
  189. {this.state.loading ? (
  190. <Button size="sm" disabled>
  191. ...
  192. </Button>
  193. ) : team.isMember ? (
  194. <Button
  195. size="sm"
  196. onClick={this.handleLeaveTeam}
  197. disabled={idpProvisioned}
  198. title={buttonHelpText}
  199. >
  200. {t('Leave Team')}
  201. </Button>
  202. ) : team.isPending ? (
  203. <Button
  204. size="sm"
  205. disabled
  206. title={t(
  207. 'Your request to join this team is being reviewed by organization owners'
  208. )}
  209. >
  210. {t('Request Pending')}
  211. </Button>
  212. ) : openMembership ? (
  213. <Button
  214. size="sm"
  215. onClick={this.handleJoinTeam}
  216. disabled={idpProvisioned}
  217. title={buttonHelpText}
  218. >
  219. {t('Join Team')}
  220. </Button>
  221. ) : (
  222. <Button
  223. size="sm"
  224. onClick={this.handleRequestAccess}
  225. disabled={idpProvisioned}
  226. title={buttonHelpText}
  227. >
  228. {t('Request Access')}
  229. </Button>
  230. )}
  231. </div>
  232. </TeamPanelItem>
  233. );
  234. }
  235. }
  236. const TeamLink = styled(Link)`
  237. display: inline-block;
  238. &.focus-visible {
  239. margin: -${space(1)};
  240. padding: ${space(1)};
  241. background: #f2eff5;
  242. border-radius: 3px;
  243. outline: none;
  244. }
  245. `;
  246. export {AllTeamsRow};
  247. export default withApi(AllTeamsRow);
  248. const TeamPanelItem = styled(PanelItem)`
  249. display: grid;
  250. grid-template-columns: minmax(150px, 4fr) minmax(90px, 1fr) min-content;
  251. gap: ${space(2)};
  252. align-items: center;
  253. > div:last-child {
  254. margin-left: auto;
  255. }
  256. `;