inviteRequestRow.tsx 5.7 KB

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