teamSelectForMember.tsx 6.1 KB

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