teamSelectForMember.tsx 6.1 KB

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