teamMembersRow.tsx 4.8 KB

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