missingProjectMembership.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {joinTeam} from 'sentry/actionCreators/teams';
  5. import {Client} from 'sentry/api';
  6. import Button from 'sentry/components/button';
  7. import EmptyMessage from 'sentry/components/emptyMessage';
  8. import SelectControl from 'sentry/components/forms/controls/selectControl';
  9. import {Panel} from 'sentry/components/panels';
  10. import {IconFlag} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import TeamStore from 'sentry/stores/teamStore';
  13. import space from 'sentry/styles/space';
  14. import {Organization, Project} from 'sentry/types';
  15. import withApi from 'sentry/utils/withApi';
  16. type Props = {
  17. api: Client;
  18. organization: Organization;
  19. project?: Project | null;
  20. };
  21. type State = {
  22. error: boolean;
  23. loading: boolean;
  24. team: string | null;
  25. project?: Project | null;
  26. };
  27. class MissingProjectMembership extends Component<Props, State> {
  28. state: State = {
  29. loading: false,
  30. error: false,
  31. project: this.props.project,
  32. team: '',
  33. };
  34. joinTeam(teamSlug: string) {
  35. this.setState({
  36. loading: true,
  37. });
  38. joinTeam(
  39. this.props.api,
  40. {
  41. orgId: this.props.organization.slug,
  42. teamId: teamSlug,
  43. },
  44. {
  45. success: () => {
  46. this.setState({
  47. loading: false,
  48. error: false,
  49. });
  50. addSuccessMessage(t('Request to join team sent.'));
  51. },
  52. error: () => {
  53. this.setState({
  54. loading: false,
  55. error: true,
  56. });
  57. addErrorMessage(t('There was an error while trying to request access.'));
  58. },
  59. }
  60. );
  61. }
  62. renderJoinTeam(teamSlug: string, features: Set<string>) {
  63. const team = TeamStore.getBySlug(teamSlug);
  64. if (!team) {
  65. return null;
  66. }
  67. if (this.state.loading) {
  68. if (features.has('open-membership')) {
  69. return <Button busy>{t('Join Team')}</Button>;
  70. }
  71. return <Button busy>{t('Request Access')}</Button>;
  72. }
  73. if (team?.isPending) {
  74. return <Button disabled>{t('Request Pending')}</Button>;
  75. }
  76. if (features.has('open-membership')) {
  77. return (
  78. <Button
  79. priority="primary"
  80. type="button"
  81. onClick={this.joinTeam.bind(this, teamSlug)}
  82. >
  83. {t('Join Team')}
  84. </Button>
  85. );
  86. }
  87. return (
  88. <Button
  89. priority="primary"
  90. type="button"
  91. onClick={this.joinTeam.bind(this, teamSlug)}
  92. >
  93. {t('Request Access')}
  94. </Button>
  95. );
  96. }
  97. getTeamsForAccess() {
  98. const request: string[] = [];
  99. const pending: string[] = [];
  100. const teams = this.state.project?.teams ?? [];
  101. teams.forEach(({slug}) => {
  102. const team = TeamStore.getBySlug(slug);
  103. if (!team) {
  104. return;
  105. }
  106. team.isPending ? pending.push(team.slug) : request.push(team.slug);
  107. });
  108. return [request, pending];
  109. }
  110. getPendingTeamOption = (team: string) => {
  111. return {
  112. value: team,
  113. label: <DisabledLabel>{`#${team}`}</DisabledLabel>,
  114. };
  115. };
  116. render() {
  117. const {organization} = this.props;
  118. const teamSlug = this.state.team;
  119. const teams = this.state.project?.teams ?? [];
  120. const features = new Set(organization.features);
  121. const teamAccess = [
  122. {
  123. label: t('Request Access'),
  124. options: this.getTeamsForAccess()[0].map(request => ({
  125. value: request,
  126. label: `#${request}`,
  127. })),
  128. },
  129. {
  130. label: t('Pending Requests'),
  131. options: this.getTeamsForAccess()[1].map(pending =>
  132. this.getPendingTeamOption(pending)
  133. ),
  134. },
  135. ];
  136. return (
  137. <StyledPanel>
  138. {!teams.length ? (
  139. <EmptyMessage icon={<IconFlag size="xl" />}>
  140. {t(
  141. 'No teams have access to this project yet. Ask an admin to add your team to this project.'
  142. )}
  143. </EmptyMessage>
  144. ) : (
  145. <EmptyMessage
  146. icon={<IconFlag size="xl" />}
  147. title={t("You're not a member of this project.")}
  148. description={t(
  149. `You'll need to join a team with access before you can view this data.`
  150. )}
  151. action={
  152. <Field>
  153. <StyledSelectControl
  154. name="select"
  155. placeholder={t('Select a Team')}
  156. options={teamAccess}
  157. onChange={teamObj => {
  158. const team = teamObj ? teamObj.value : null;
  159. this.setState({team});
  160. }}
  161. />
  162. {teamSlug ? (
  163. this.renderJoinTeam(teamSlug, features)
  164. ) : (
  165. <Button disabled>{t('Select a Team')}</Button>
  166. )}
  167. </Field>
  168. }
  169. />
  170. )}
  171. </StyledPanel>
  172. );
  173. }
  174. }
  175. const StyledPanel = styled(Panel)`
  176. margin: ${space(2)} 0;
  177. `;
  178. const Field = styled('div')`
  179. display: grid;
  180. grid-auto-flow: column;
  181. gap: ${space(2)};
  182. text-align: left;
  183. `;
  184. const StyledSelectControl = styled(SelectControl)`
  185. width: 250px;
  186. `;
  187. const DisabledLabel = styled('div')`
  188. display: flex;
  189. opacity: 0.5;
  190. overflow: hidden;
  191. `;
  192. export default withApi(MissingProjectMembership);