teamMembersRow.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import styled from '@emotion/styled';
  2. import {Button} from 'sentry/components/button';
  3. import IdBadge from 'sentry/components/idBadge';
  4. import {PanelItem} from 'sentry/components/panels';
  5. import RoleSelectControl from 'sentry/components/roleSelectControl';
  6. import {IconSubtract} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {Member, Organization, Team, TeamMember, User} from 'sentry/types';
  10. import {getEffectiveOrgRole} from 'sentry/utils/orgRole';
  11. import {
  12. hasOrgRoleOverwrite,
  13. RoleOverwriteIcon,
  14. } from 'sentry/views/settings/organizationTeams/roleOverwriteWarning';
  15. import {getButtonHelpText} from 'sentry/views/settings/organizationTeams/utils';
  16. const TeamMembersRow = (props: {
  17. hasWriteAccess: boolean;
  18. isOrgOwner: boolean;
  19. member: TeamMember;
  20. organization: Organization;
  21. removeMember: (member: Member) => void;
  22. team: Team;
  23. updateMemberRole: (member: Member, newRole: string) => void;
  24. user: User;
  25. }) => {
  26. const {
  27. organization,
  28. team,
  29. member,
  30. user,
  31. hasWriteAccess,
  32. isOrgOwner,
  33. removeMember,
  34. updateMemberRole,
  35. } = props;
  36. return (
  37. <TeamRolesPanelItem key={member.id}>
  38. <div>
  39. <IdBadge avatarSize={36} member={member} useLink orgId={organization.slug} />
  40. </div>
  41. <div>
  42. <TeamRoleSelect
  43. hasWriteAccess={hasWriteAccess}
  44. updateMemberRole={updateMemberRole}
  45. organization={organization}
  46. team={team}
  47. member={member}
  48. />
  49. </div>
  50. <div>
  51. <RemoveButton
  52. hasWriteAccess={hasWriteAccess}
  53. hasOrgRoleFromTeam={team.orgRole !== null}
  54. isOrgOwner={isOrgOwner}
  55. onClick={() => removeMember(member)}
  56. member={member}
  57. user={user}
  58. />
  59. </div>
  60. </TeamRolesPanelItem>
  61. );
  62. };
  63. const TeamRoleSelect = (props: {
  64. hasWriteAccess: boolean;
  65. member: TeamMember;
  66. organization: Organization;
  67. team: Team;
  68. updateMemberRole: (member: TeamMember, newRole: string) => void;
  69. }) => {
  70. const {hasWriteAccess, organization, team, member, updateMemberRole} = props;
  71. const {orgRoleList, teamRoleList, features} = organization;
  72. if (!features.includes('team-roles')) {
  73. return null;
  74. }
  75. // Determine the team-role, including if the current team has an org role
  76. // and if adding the user to the current team changes their minimum team-role
  77. const possibleOrgRoles = [member.orgRole];
  78. if (member.orgRolesFromTeams && member.orgRolesFromTeams.length > 0) {
  79. possibleOrgRoles.push(member.orgRolesFromTeams[0].role.id);
  80. }
  81. if (team.orgRole) {
  82. possibleOrgRoles.push(team.orgRole);
  83. }
  84. const effectiveOrgRole = getEffectiveOrgRole(possibleOrgRoles, orgRoleList);
  85. const teamRoleId = member.teamRole || effectiveOrgRole?.minimumTeamRole;
  86. const teamRole = teamRoleList.find(r => r.id === teamRoleId) || teamRoleList[0];
  87. if (
  88. !hasWriteAccess ||
  89. hasOrgRoleOverwrite({orgRole: effectiveOrgRole?.id, orgRoleList, teamRoleList})
  90. ) {
  91. return (
  92. <RoleName>
  93. {teamRole.name}
  94. <IconWrapper>
  95. <RoleOverwriteIcon
  96. orgRole={effectiveOrgRole?.id}
  97. orgRoleList={orgRoleList}
  98. teamRoleList={teamRoleList}
  99. />
  100. </IconWrapper>
  101. </RoleName>
  102. );
  103. }
  104. return (
  105. <RoleSelectWrapper>
  106. <RoleSelectControl
  107. roles={teamRoleList}
  108. value={teamRole.id}
  109. onChange={option => updateMemberRole(member, option.value)}
  110. disableUnallowed
  111. />
  112. </RoleSelectWrapper>
  113. );
  114. };
  115. const RemoveButton = (props: {
  116. hasOrgRoleFromTeam: boolean;
  117. hasWriteAccess: boolean;
  118. isOrgOwner: boolean;
  119. member: TeamMember;
  120. onClick: () => void;
  121. user: User;
  122. }) => {
  123. const {member, user, hasWriteAccess, isOrgOwner, hasOrgRoleFromTeam, onClick} = props;
  124. const isSelf = member.email === user.email;
  125. const canRemoveMember = hasWriteAccess || isSelf;
  126. if (!canRemoveMember) {
  127. return null;
  128. }
  129. const isIdpProvisioned = member.flags['idp:provisioned'];
  130. const isPermissionGroup = hasOrgRoleFromTeam && !isOrgOwner;
  131. const buttonHelpText = getButtonHelpText(isIdpProvisioned, isPermissionGroup);
  132. if (isIdpProvisioned || isPermissionGroup) {
  133. return (
  134. <Button
  135. size="xs"
  136. disabled
  137. icon={<IconSubtract size="xs" isCircled />}
  138. onClick={onClick}
  139. aria-label={t('Remove')}
  140. title={buttonHelpText}
  141. >
  142. {t('Remove')}
  143. </Button>
  144. );
  145. }
  146. return (
  147. <Button
  148. size="xs"
  149. disabled={!canRemoveMember}
  150. icon={<IconSubtract size="xs" isCircled />}
  151. onClick={onClick}
  152. aria-label={t('Remove')}
  153. >
  154. {t('Remove')}
  155. </Button>
  156. );
  157. };
  158. const RoleName = styled('div')`
  159. display: flex;
  160. align-items: center;
  161. `;
  162. const IconWrapper = styled('div')`
  163. height: ${space(2)};
  164. margin-left: ${space(1)};
  165. `;
  166. const RoleSelectWrapper = styled('div')`
  167. display: flex;
  168. flex-direction: row;
  169. align-items: center;
  170. > div:first-child {
  171. flex-grow: 1;
  172. }
  173. `;
  174. const TeamRolesPanelItem = styled(PanelItem)`
  175. display: grid;
  176. grid-template-columns: minmax(120px, 4fr) minmax(120px, 2fr) minmax(100px, 1fr);
  177. gap: ${space(2)};
  178. align-items: center;
  179. > div:last-child {
  180. margin-left: auto;
  181. }
  182. `;
  183. export default TeamMembersRow;