inviteRequestRow.tsx 5.8 KB

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