inviteRequestRow.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import Confirm from 'sentry/components/confirm';
  5. import {
  6. InviteModalHook,
  7. InviteModalRenderFunc,
  8. } from 'sentry/components/modals/memberInviteModalCustomization';
  9. import PanelItem from 'sentry/components/panels/panelItem';
  10. import RoleSelectControl from 'sentry/components/roleSelectControl';
  11. import Tag from 'sentry/components/tag';
  12. import TeamSelector from 'sentry/components/teamSelector';
  13. import {Tooltip} from 'sentry/components/tooltip';
  14. import {IconCheckmark, IconClose} from 'sentry/icons';
  15. import {t, tct} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import {Member, Organization, OrgRole} from 'sentry/types';
  18. type Props = {
  19. allRoles: OrgRole[];
  20. inviteRequest: Member;
  21. inviteRequestBusy: {[key: string]: boolean};
  22. onApprove: (inviteRequest: Member) => void;
  23. onDeny: (inviteRequest: Member) => void;
  24. onUpdate: (data: Partial<Member>) => void;
  25. organization: Organization;
  26. };
  27. function InviteRequestRow({
  28. inviteRequest,
  29. inviteRequestBusy,
  30. organization,
  31. onApprove,
  32. onDeny,
  33. onUpdate,
  34. allRoles,
  35. }: Props) {
  36. const role = allRoles.find(r => r.id === inviteRequest.role);
  37. const roleDisallowed = !(role && role.allowed);
  38. const {access} = organization;
  39. const canApprove = access.includes('member:admin');
  40. const hookRenderer: InviteModalRenderFunc = ({sendInvites, canSend, headerInfo}) => (
  41. <StyledPanelItem>
  42. <div>
  43. <h5 style={{marginBottom: space(0.5)}}>
  44. <UserName>{inviteRequest.email}</UserName>
  45. </h5>
  46. {inviteRequest.inviteStatus === 'requested_to_be_invited' ? (
  47. inviteRequest.inviterName && (
  48. <Description>
  49. <Tooltip
  50. title={t(
  51. 'An existing member has asked to invite this user to your organization'
  52. )}
  53. >
  54. {tct('Requested by [inviterName]', {
  55. inviterName: inviteRequest.inviterName,
  56. })}
  57. </Tooltip>
  58. </Description>
  59. )
  60. ) : (
  61. <JoinRequestIndicator
  62. tooltipText={t('This user has asked to join your organization.')}
  63. >
  64. {t('Join request')}
  65. </JoinRequestIndicator>
  66. )}
  67. </div>
  68. {canApprove ? (
  69. <StyledRoleSelectControl
  70. name="role"
  71. disableUnallowed
  72. onChange={r => onUpdate({role: r.value})}
  73. value={inviteRequest.role}
  74. roles={allRoles}
  75. />
  76. ) : (
  77. <div>{inviteRequest.roleName}</div>
  78. )}
  79. {canApprove ? (
  80. <TeamSelectControl
  81. name="teams"
  82. placeholder={t('None')}
  83. onChange={teams => onUpdate({teams: (teams || []).map(team => team.value)})}
  84. value={inviteRequest.teams}
  85. clearable
  86. multiple
  87. />
  88. ) : (
  89. <div>{inviteRequest.teams.join(', ')}</div>
  90. )}
  91. <ButtonGroup>
  92. <Button
  93. size="sm"
  94. busy={inviteRequestBusy[inviteRequest.id]}
  95. onClick={() => onDeny(inviteRequest)}
  96. icon={<IconClose />}
  97. disabled={!canApprove}
  98. title={
  99. canApprove
  100. ? undefined
  101. : t('This request needs to be reviewed by a privileged user')
  102. }
  103. >
  104. {t('Deny')}
  105. </Button>
  106. <Confirm
  107. onConfirm={sendInvites}
  108. disableConfirmButton={!canSend}
  109. disabled={!canApprove || roleDisallowed}
  110. message={
  111. <Fragment>
  112. {tct('Are you sure you want to invite [email] to your organization?', {
  113. email: inviteRequest.email,
  114. })}
  115. {headerInfo}
  116. </Fragment>
  117. }
  118. >
  119. <Button
  120. priority="primary"
  121. size="sm"
  122. busy={inviteRequestBusy[inviteRequest.id]}
  123. title={
  124. canApprove
  125. ? roleDisallowed
  126. ? t(
  127. `You do not have permission to approve a user of this role.
  128. Select a different role to approve this user.`
  129. )
  130. : undefined
  131. : t('This request needs to be reviewed by a privileged user')
  132. }
  133. icon={<IconCheckmark />}
  134. >
  135. {t('Approve')}
  136. </Button>
  137. </Confirm>
  138. </ButtonGroup>
  139. </StyledPanelItem>
  140. );
  141. return (
  142. <InviteModalHook
  143. willInvite
  144. organization={organization}
  145. onSendInvites={() => onApprove(inviteRequest)}
  146. >
  147. {hookRenderer}
  148. </InviteModalHook>
  149. );
  150. }
  151. const JoinRequestIndicator = styled(Tag)`
  152. text-transform: uppercase;
  153. `;
  154. const StyledPanelItem = styled(PanelItem)`
  155. display: grid;
  156. grid-template-columns: minmax(150px, auto) minmax(100px, 140px) 220px max-content;
  157. gap: ${space(2)};
  158. align-items: center;
  159. `;
  160. const UserName = styled('div')`
  161. font-size: ${p => p.theme.fontSizeLarge};
  162. overflow: hidden;
  163. text-overflow: ellipsis;
  164. `;
  165. const Description = styled('div')`
  166. display: block;
  167. color: ${p => p.theme.subText};
  168. font-size: 14px;
  169. overflow: hidden;
  170. text-overflow: ellipsis;
  171. `;
  172. const StyledRoleSelectControl = styled(RoleSelectControl)`
  173. max-width: 140px;
  174. `;
  175. const TeamSelectControl = styled(TeamSelector)`
  176. max-width: 220px;
  177. .Select-value-label {
  178. max-width: 150px;
  179. word-break: break-all;
  180. }
  181. `;
  182. const ButtonGroup = styled('div')`
  183. display: inline-grid;
  184. grid-template-columns: repeat(2, max-content);
  185. gap: ${space(1)};
  186. `;
  187. export default InviteRequestRow;