missingProjectMembership.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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/panel';
  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: string[]) {
  63. const team = TeamStore.getBySlug(teamSlug);
  64. if (!team) {
  65. return null;
  66. }
  67. if (this.state.loading) {
  68. if (features.includes('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.includes('open-membership')) {
  77. return (
  78. <Button priority="primary" onClick={this.joinTeam.bind(this, teamSlug)}>
  79. {t('Join Team')}
  80. </Button>
  81. );
  82. }
  83. return (
  84. <Button priority="primary" onClick={this.joinTeam.bind(this, teamSlug)}>
  85. {t('Request Access')}
  86. </Button>
  87. );
  88. }
  89. getTeamsForAccess() {
  90. const request: string[] = [];
  91. const pending: string[] = [];
  92. const teams = this.state.project?.teams ?? [];
  93. teams.forEach(({slug}) => {
  94. const team = TeamStore.getBySlug(slug);
  95. if (!team) {
  96. return;
  97. }
  98. team.isPending ? pending.push(team.slug) : request.push(team.slug);
  99. });
  100. return [request, pending];
  101. }
  102. getPendingTeamOption = (team: string) => {
  103. return {
  104. value: team,
  105. label: <DisabledLabel>{`#${team}`}</DisabledLabel>,
  106. };
  107. };
  108. render() {
  109. const {organization} = this.props;
  110. const teamSlug = this.state.team;
  111. const teams = this.state.project?.teams ?? [];
  112. const teamAccess = [
  113. {
  114. label: t('Request Access'),
  115. options: this.getTeamsForAccess()[0].map(request => ({
  116. value: request,
  117. label: `#${request}`,
  118. })),
  119. },
  120. {
  121. label: t('Pending Requests'),
  122. options: this.getTeamsForAccess()[1].map(pending =>
  123. this.getPendingTeamOption(pending)
  124. ),
  125. },
  126. ];
  127. return (
  128. <StyledPanel>
  129. {!teams.length ? (
  130. <EmptyMessage icon={<IconFlag size="xl" />}>
  131. {t(
  132. 'No teams have access to this project yet. Ask an admin to add your team to this project.'
  133. )}
  134. </EmptyMessage>
  135. ) : (
  136. <EmptyMessage
  137. icon={<IconFlag size="xl" />}
  138. title={t("You're not a member of this project.")}
  139. description={t(
  140. `You'll need to join a team with access before you can view this data.`
  141. )}
  142. action={
  143. <Field>
  144. <StyledSelectControl
  145. name="select"
  146. placeholder={t('Select a Team')}
  147. options={teamAccess}
  148. onChange={teamObj => {
  149. const team = teamObj ? teamObj.value : null;
  150. this.setState({team});
  151. }}
  152. />
  153. {teamSlug ? (
  154. this.renderJoinTeam(teamSlug, organization.features)
  155. ) : (
  156. <Button disabled>{t('Select a Team')}</Button>
  157. )}
  158. </Field>
  159. }
  160. />
  161. )}
  162. </StyledPanel>
  163. );
  164. }
  165. }
  166. const StyledPanel = styled(Panel)`
  167. margin: ${space(2)} 0;
  168. `;
  169. const Field = styled('div')`
  170. display: grid;
  171. grid-auto-flow: column;
  172. gap: ${space(2)};
  173. text-align: left;
  174. `;
  175. const StyledSelectControl = styled(SelectControl)`
  176. width: 250px;
  177. `;
  178. const DisabledLabel = styled('div')`
  179. display: flex;
  180. opacity: 0.5;
  181. overflow: hidden;
  182. `;
  183. export default withApi(MissingProjectMembership);