teamSelectForMember.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import startCase from 'lodash/startCase';
  4. import {Button} from 'sentry/components/button';
  5. import EmptyMessage from 'sentry/components/emptyMessage';
  6. import {TeamBadge} from 'sentry/components/idBadge/teamBadge';
  7. import Link from 'sentry/components/links/link';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {Panel, PanelBody, PanelHeader, PanelItem} from 'sentry/components/panels';
  10. import TeamRoleSelect from 'sentry/components/teamRoleSelect';
  11. import {IconSubtract} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {Member, Organization, Team} from 'sentry/types';
  15. import {getEffectiveOrgRole} from 'sentry/utils/orgRole';
  16. import {useTeams} from 'sentry/utils/useTeams';
  17. import {RoleOverwritePanelAlert} from 'sentry/views/settings/organizationTeams/roleOverwriteWarning';
  18. import {getButtonHelpText} from 'sentry/views/settings/organizationTeams/utils';
  19. import {DropdownAddTeam, TeamSelectProps} from './utils';
  20. type Props = TeamSelectProps & {
  21. /**
  22. * Member that this component is acting upon
  23. */
  24. member: Member;
  25. /**
  26. * Used when showing Teams for a Member
  27. */
  28. onChangeTeamRole: (teamSlug: string, teamRole: string) => void;
  29. /**
  30. * Used when showing Teams for a Member
  31. */
  32. selectedOrgRole: Member['orgRole'];
  33. /**
  34. * Used when showing Teams for a Member
  35. */
  36. selectedTeamRoles: Member['teamRoles'];
  37. };
  38. function TeamSelect({
  39. disabled,
  40. loadingTeams,
  41. member,
  42. selectedOrgRole,
  43. selectedTeamRoles,
  44. organization,
  45. onAddTeam,
  46. onRemoveTeam,
  47. onCreateTeam,
  48. onChangeTeamRole,
  49. }: Props) {
  50. const {teams, onSearch, fetching: isLoadingTeams} = useTeams();
  51. const {orgRoleList, teamRoleList} = organization;
  52. const selectedTeamSlugs = new Set(selectedTeamRoles.map(tm => tm.teamSlug));
  53. const selectedTeams = teams.filter(tm => selectedTeamSlugs.has(tm.slug));
  54. // Determine if adding a team changes the minimum team-role
  55. // Get org-roles from team membership, if any
  56. const groupOrgRoles = selectedTeams
  57. .filter(team => team.orgRole)
  58. .map(team => team.orgRole as string);
  59. if (selectedOrgRole) {
  60. groupOrgRoles.push(selectedOrgRole);
  61. }
  62. // Sort them and to get the highest priority role
  63. // Highest priority role may change minimum team role
  64. const effectiveOrgRole = getEffectiveOrgRole(groupOrgRoles, orgRoleList);
  65. const renderBody = () => {
  66. if (selectedTeams.length === 0) {
  67. return <EmptyMessage>{t('No Teams assigned')}</EmptyMessage>;
  68. }
  69. return (
  70. <React.Fragment>
  71. {organization.features.includes('team-roles') && effectiveOrgRole && (
  72. <RoleOverwritePanelAlert
  73. orgRole={effectiveOrgRole?.id}
  74. orgRoleList={orgRoleList}
  75. teamRoleList={teamRoleList}
  76. />
  77. )}
  78. {selectedTeams.map(team => (
  79. <TeamRow
  80. key={team.slug}
  81. disabled={disabled}
  82. organization={organization}
  83. team={team}
  84. member={{
  85. ...member,
  86. groupOrgRoles: [{role: effectiveOrgRole, teamSlug: ''}],
  87. orgRole: selectedOrgRole,
  88. teamRoles: selectedTeamRoles,
  89. }}
  90. onChangeTeamRole={onChangeTeamRole}
  91. onRemoveTeam={slug => onRemoveTeam(slug)}
  92. />
  93. ))}
  94. </React.Fragment>
  95. );
  96. };
  97. return (
  98. <Panel>
  99. <PanelHeader hasButtons>
  100. {t('Team')}
  101. <DropdownAddTeam
  102. disabled={disabled}
  103. isLoadingTeams={isLoadingTeams}
  104. isAddingTeamToMember
  105. canCreateTeam={false}
  106. onSearch={onSearch}
  107. onSelect={onAddTeam}
  108. onCreateTeam={onCreateTeam}
  109. organization={organization}
  110. selectedTeams={selectedTeams.map(tm => tm.slug)}
  111. teams={teams}
  112. />
  113. </PanelHeader>
  114. <PanelBody>{loadingTeams ? <LoadingIndicator /> : renderBody()}</PanelBody>
  115. </Panel>
  116. );
  117. }
  118. function TeamRow({
  119. disabled,
  120. organization,
  121. team,
  122. member,
  123. onRemoveTeam,
  124. onChangeTeamRole,
  125. }: {
  126. disabled: boolean;
  127. member: Member;
  128. onChangeTeamRole: Props['onChangeTeamRole'];
  129. onRemoveTeam: Props['onRemoveTeam'];
  130. organization: Organization;
  131. team: Team;
  132. }) {
  133. const hasOrgAdmin = organization.access.includes('org:admin');
  134. const isIdpProvisioned = team.flags['idp:provisioned'];
  135. const isPermissionGroup = team.orgRole !== null && !hasOrgAdmin;
  136. const isRemoveDisabled = disabled || isIdpProvisioned || isPermissionGroup;
  137. const buttonHelpText = getButtonHelpText(isIdpProvisioned, isPermissionGroup);
  138. const orgRoleFromTeam = team.orgRole ? `${startCase(team.orgRole)} Team` : null;
  139. return (
  140. <TeamPanelItem data-test-id="team-row-for-member">
  141. <TeamPanelItemLeft>
  142. <Link to={`/settings/${organization.slug}/teams/${team.slug}/`}>
  143. <TeamBadge team={team} />
  144. </Link>
  145. </TeamPanelItemLeft>
  146. <TeamOrgRole>{orgRoleFromTeam}</TeamOrgRole>
  147. {organization.features.includes('team-roles') && (
  148. <RoleSelectWrapper>
  149. <TeamRoleSelect
  150. disabled={disabled}
  151. size="xs"
  152. organization={organization}
  153. team={team}
  154. member={member}
  155. onChangeTeamRole={newRole => onChangeTeamRole(team.slug, newRole)}
  156. />
  157. </RoleSelectWrapper>
  158. )}
  159. <Button
  160. size="xs"
  161. icon={<IconSubtract isCircled size="xs" />}
  162. title={buttonHelpText}
  163. disabled={isRemoveDisabled}
  164. onClick={() => onRemoveTeam(team.slug)}
  165. >
  166. {t('Remove')}
  167. </Button>
  168. </TeamPanelItem>
  169. );
  170. }
  171. const TeamPanelItem = styled(PanelItem)`
  172. padding: ${space(2)};
  173. align-items: center;
  174. justify-content: space-between;
  175. `;
  176. const TeamPanelItemLeft = styled('div')`
  177. flex-grow: 4;
  178. `;
  179. const TeamOrgRole = styled('div')`
  180. min-width: 90px;
  181. flex-grow: 1;
  182. display: flex;
  183. justify-content: center;
  184. `;
  185. const RoleSelectWrapper = styled('div')`
  186. min-width: 200px;
  187. margin-right: ${space(2)};
  188. `;
  189. export default TeamSelect;