utils.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import React from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import debounce from 'lodash/debounce';
  5. import {openCreateTeamModal} from 'sentry/actionCreators/modal';
  6. import {hasEveryAccess} from 'sentry/components/acl/access';
  7. import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
  8. import {Item} from 'sentry/components/dropdownAutoComplete/types';
  9. import DropdownButton from 'sentry/components/dropdownButton';
  10. import {TeamBadge} from 'sentry/components/idBadge/teamBadge';
  11. import Link from 'sentry/components/links/link';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import {Organization, Project, Team} from 'sentry/types';
  17. import {getButtonHelpText} from 'sentry/views/settings/organizationTeams/utils';
  18. export type TeamSelectProps = {
  19. /**
  20. * Should button be disabled
  21. */
  22. disabled: boolean;
  23. /**
  24. * callback when teams are added
  25. */
  26. onAddTeam: (teamSlug: string) => void;
  27. /**
  28. * Callback when teams are removed
  29. */
  30. onRemoveTeam: (teamSlug: string) => void;
  31. organization: Organization;
  32. /**
  33. * Used to determine whether we should show a loading state while waiting for teams
  34. */
  35. loadingTeams?: boolean;
  36. /**
  37. * Callback when teams are created
  38. */
  39. onCreateTeam?: (team: Team) => void;
  40. };
  41. export function DropdownAddTeam({
  42. disabled,
  43. isLoadingTeams,
  44. isAddingTeamToMember = false,
  45. isAddingTeamToProject = false,
  46. onSearch,
  47. onSelect,
  48. onCreateTeam,
  49. organization,
  50. selectedTeams,
  51. teams,
  52. project,
  53. }: {
  54. disabled: boolean;
  55. isLoadingTeams: boolean;
  56. onSearch: (teamSlug: string) => void;
  57. onSelect: (teamSlug: string) => void;
  58. organization: Organization;
  59. selectedTeams: string[];
  60. teams: Team[];
  61. canCreateTeam?: boolean;
  62. isAddingTeamToMember?: boolean;
  63. isAddingTeamToProject?: boolean;
  64. onCreateTeam?: (team: Team) => void;
  65. project?: Project;
  66. }) {
  67. const dropdownItems = teams
  68. .filter(team => !selectedTeams.some(slug => slug === team.slug))
  69. .map((team, index) =>
  70. renderDropdownOption({
  71. isAddingTeamToMember,
  72. isAddingTeamToProject,
  73. organization,
  74. team,
  75. index,
  76. disabled,
  77. })
  78. );
  79. const onDropdownChange = debounce<(e: React.ChangeEvent<HTMLInputElement>) => void>(
  80. e => onSearch(e.target.value),
  81. DEFAULT_DEBOUNCE_DURATION
  82. );
  83. return (
  84. <DropdownAutoComplete
  85. items={dropdownItems}
  86. busyItemsStillVisible={isLoadingTeams}
  87. onChange={onDropdownChange}
  88. onSelect={(option: Item) => onSelect(option.value)}
  89. emptyMessage={t('No teams')}
  90. menuHeader={renderDropdownHeader({
  91. organization,
  92. project,
  93. onCreateTeam,
  94. })}
  95. disabled={disabled}
  96. alignMenu="right"
  97. >
  98. {({isOpen}) => (
  99. <DropdownButton
  100. aria-label={t('Add Team')}
  101. isOpen={isOpen}
  102. size="xs"
  103. disabled={disabled}
  104. >
  105. {t('Add Team')}
  106. </DropdownButton>
  107. )}
  108. </DropdownAutoComplete>
  109. );
  110. }
  111. function renderDropdownOption({
  112. disabled,
  113. index,
  114. isAddingTeamToMember,
  115. organization,
  116. team,
  117. }: {
  118. disabled: boolean;
  119. index: number;
  120. isAddingTeamToMember: boolean;
  121. isAddingTeamToProject: boolean;
  122. organization: Organization;
  123. team: Team;
  124. }) {
  125. const hasOrgAdmin = organization.access.includes('org:admin');
  126. const isIdpProvisioned = isAddingTeamToMember && team.flags['idp:provisioned'];
  127. const isPermissionGroup = isAddingTeamToMember && team.orgRole !== null && !hasOrgAdmin;
  128. const buttonHelpText = getButtonHelpText(isIdpProvisioned, isPermissionGroup);
  129. return {
  130. index,
  131. value: team.slug,
  132. searchKey: team.slug,
  133. label: () => {
  134. if (isIdpProvisioned || isPermissionGroup) {
  135. return (
  136. <Tooltip title={buttonHelpText}>
  137. <DropdownTeamBadgeDisabled avatarSize={18} team={team} />
  138. </Tooltip>
  139. );
  140. }
  141. return <DropdownTeamBadge avatarSize={18} team={team} />;
  142. },
  143. disabled: disabled || isIdpProvisioned || isPermissionGroup,
  144. };
  145. }
  146. function renderDropdownHeader({
  147. organization,
  148. project,
  149. onCreateTeam,
  150. }: {
  151. organization: Organization;
  152. onCreateTeam?: (team) => void;
  153. project?: Project;
  154. }) {
  155. const canCreateTeam = hasEveryAccess(['org:write'], {organization, project});
  156. return (
  157. <StyledTeamsLabel>
  158. <span>{t('Teams')}</span>
  159. <Tooltip
  160. disabled={canCreateTeam}
  161. title={t('You must be a Org Owner/Manager to create teams')}
  162. position="top"
  163. >
  164. <StyledCreateTeamLink
  165. to="#create-team"
  166. disabled={!canCreateTeam}
  167. onClick={(e: React.MouseEvent) => {
  168. e.stopPropagation();
  169. e.preventDefault();
  170. openCreateTeamModal({
  171. organization,
  172. project,
  173. onClose: onCreateTeam,
  174. });
  175. }}
  176. >
  177. {t('Create Team')}
  178. </StyledCreateTeamLink>
  179. </Tooltip>
  180. </StyledTeamsLabel>
  181. );
  182. }
  183. const DropdownTeamBadge = styled(TeamBadge)`
  184. font-weight: normal;
  185. font-size: ${p => p.theme.fontSizeMedium};
  186. text-transform: none;
  187. `;
  188. const DropdownTeamBadgeDisabled = styled(TeamBadge)`
  189. font-weight: normal;
  190. font-size: ${p => p.theme.fontSizeMedium};
  191. text-transform: none;
  192. filter: grayscale(1);
  193. `;
  194. const StyledTeamsLabel = styled('div')`
  195. display: flex;
  196. flex-direction: row;
  197. justify-content: space-between;
  198. font-size: 0.875em;
  199. padding: ${space(0.5)} 0px;
  200. text-transform: uppercase;
  201. `;
  202. const StyledCreateTeamLink = styled(Link)`
  203. float: right;
  204. text-transform: none;
  205. ${p =>
  206. p.disabled &&
  207. css`
  208. cursor: not-allowed;
  209. color: ${p.theme.gray300};
  210. opacity: 0.6;
  211. `};
  212. `;