teamSelectForProject.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import Confirm from 'sentry/components/confirm';
  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 {IconSubtract} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Organization, Team} from 'sentry/types/organization';
  17. import type {Project} from 'sentry/types/project';
  18. import {useTeams} from 'sentry/utils/useTeams';
  19. import type {TeamSelectProps} from './utils';
  20. import {DropdownAddTeam} from './utils';
  21. type Props = TeamSelectProps & {
  22. canCreateTeam: boolean;
  23. project: Project;
  24. /**
  25. * Used when showing Teams for a Project
  26. */
  27. selectedTeams: Team[];
  28. };
  29. function TeamSelect({
  30. disabled,
  31. canCreateTeam,
  32. project,
  33. selectedTeams,
  34. organization,
  35. onAddTeam,
  36. onRemoveTeam,
  37. onCreateTeam,
  38. }: Props) {
  39. const renderBody = () => {
  40. const numTeams = selectedTeams.length;
  41. if (numTeams === 0) {
  42. return <EmptyMessage>{t('No Teams assigned')}</EmptyMessage>;
  43. }
  44. // If the user is not a team-admin in any parent teams of this project, they will
  45. // not be able to edit the configuration. Warn the user if this is their last team
  46. // where they have team-admin role.
  47. const isUserLastTeamWrite =
  48. selectedTeams.reduce(
  49. (count, team) => (team.access.includes('team:write') ? count + 1 : count),
  50. 0
  51. ) === 1;
  52. const isOnlyTeam = numTeams === 1;
  53. const confirmMessage = isUserLastTeamWrite
  54. ? t(
  55. "This is the last team that grants Team Admin access to you for this project. After removing this team, you will not be able to edit this project's configuration."
  56. )
  57. : isOnlyTeam
  58. ? t(
  59. 'This is the last team with access to this project. After removing this team, only organization owners and managers will be able to access the project pages.'
  60. )
  61. : t(
  62. 'Removing this team from the project means that members of the team can no longer access this project. Do you want to continue?'
  63. );
  64. return (
  65. <Fragment>
  66. {selectedTeams.map(team => (
  67. <TeamRow
  68. key={team.slug}
  69. disabled={disabled || !team.access.includes('team:write')}
  70. confirmMessage={confirmMessage}
  71. organization={organization}
  72. team={team}
  73. onRemoveTeam={slug => onRemoveTeam(slug)}
  74. />
  75. ))}
  76. </Fragment>
  77. );
  78. };
  79. const {teams, onSearch, fetching: isLoadingTeams} = useTeams();
  80. return (
  81. <Panel>
  82. <PanelHeader hasButtons>
  83. {t('Team')}
  84. <DropdownAddTeam
  85. disabled={disabled}
  86. isLoadingTeams={isLoadingTeams}
  87. isAddingTeamToProject
  88. canCreateTeam={canCreateTeam}
  89. onSearch={onSearch}
  90. onSelect={onAddTeam}
  91. onCreateTeam={onCreateTeam}
  92. organization={organization}
  93. selectedTeams={selectedTeams.map(tm => tm.slug)}
  94. teams={teams}
  95. project={project}
  96. />
  97. </PanelHeader>
  98. <PanelBody>{isLoadingTeams ? <LoadingIndicator /> : renderBody()}</PanelBody>
  99. </Panel>
  100. );
  101. }
  102. function TeamRow({
  103. organization,
  104. team,
  105. onRemoveTeam,
  106. disabled,
  107. confirmMessage,
  108. }: {
  109. confirmMessage: string | null;
  110. disabled: boolean;
  111. onRemoveTeam: Props['onRemoveTeam'];
  112. organization: Organization;
  113. team: Team;
  114. }) {
  115. return (
  116. <TeamPanelItem data-test-id="team-row-for-project">
  117. <TeamPanelItemLeft>
  118. <Link to={`/settings/${organization.slug}/teams/${team.slug}/`}>
  119. <TeamBadge team={team} />
  120. </Link>
  121. </TeamPanelItemLeft>
  122. <Confirm
  123. message={confirmMessage}
  124. bypass={!confirmMessage}
  125. onConfirm={() => onRemoveTeam(team.slug)}
  126. disabled={disabled}
  127. confirmText="Remove Team"
  128. >
  129. <Button size="xs" icon={<IconSubtract isCircled />} disabled={disabled}>
  130. {t('Remove')}
  131. </Button>
  132. </Confirm>
  133. </TeamPanelItem>
  134. );
  135. }
  136. const TeamPanelItem = styled(PanelItem)`
  137. padding: ${space(2)};
  138. align-items: center;
  139. justify-content: space-between;
  140. `;
  141. const TeamPanelItemLeft = styled('div')`
  142. flex-grow: 4;
  143. `;
  144. export default TeamSelect;