projectTeams.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import {RouteComponentProps} from 'react-router';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  5. import {openCreateTeamModal} from 'sentry/actionCreators/modal';
  6. import {addTeamToProject, removeTeamFromProject} from 'sentry/actionCreators/projects';
  7. import Link from 'sentry/components/links/link';
  8. import Tooltip from 'sentry/components/tooltip';
  9. import {t} from 'sentry/locale';
  10. import space from 'sentry/styles/space';
  11. import {Organization, Project, Team} from 'sentry/types';
  12. import routeTitleGen from 'sentry/utils/routeTitle';
  13. import AsyncView from 'sentry/views/asyncView';
  14. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  15. import TeamSelect from 'sentry/views/settings/components/teamSelect';
  16. type Props = {
  17. organization: Organization;
  18. project: Project;
  19. } & RouteComponentProps<{orgId: string; projectId: string}, {}>;
  20. type State = {
  21. projectTeams: null | Team[];
  22. } & AsyncView['state'];
  23. class ProjectTeams extends AsyncView<Props, State> {
  24. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  25. const {orgId, projectId} = this.props.params;
  26. return [['projectTeams', `/projects/${orgId}/${projectId}/teams/`]];
  27. }
  28. getTitle() {
  29. const {projectId} = this.props.params;
  30. return routeTitleGen(t('Project Teams'), projectId, false);
  31. }
  32. canCreateTeam = () => {
  33. const {organization} = this.props;
  34. const access = new Set(organization.access);
  35. return (
  36. access.has('org:write') && access.has('team:write') && access.has('project:write')
  37. );
  38. };
  39. handleRemove = (teamSlug: Team['slug']) => {
  40. if (this.state.loading) {
  41. return;
  42. }
  43. const {orgId, projectId} = this.props.params;
  44. removeTeamFromProject(this.api, orgId, projectId, teamSlug)
  45. .then(() => this.handleRemovedTeam(teamSlug))
  46. .catch(() => {
  47. addErrorMessage(t('Could not remove the %s team', teamSlug));
  48. this.setState({loading: false});
  49. });
  50. };
  51. handleRemovedTeam = (teamSlug: Team['slug']) => {
  52. this.setState(prevState => ({
  53. projectTeams: [
  54. ...(prevState.projectTeams || []).filter(team => team.slug !== teamSlug),
  55. ],
  56. }));
  57. };
  58. handleAddedTeam = (team: Team) => {
  59. this.setState(prevState => ({
  60. projectTeams: [...(prevState.projectTeams || []), team],
  61. }));
  62. };
  63. handleAdd = (team: Team) => {
  64. if (this.state.loading) {
  65. return;
  66. }
  67. const {orgId, projectId} = this.props.params;
  68. addTeamToProject(this.api, orgId, projectId, team).then(
  69. () => {
  70. this.handleAddedTeam(team);
  71. },
  72. () => {
  73. this.setState({
  74. error: true,
  75. loading: false,
  76. });
  77. }
  78. );
  79. };
  80. handleCreateTeam = (e: React.MouseEvent) => {
  81. const {project, organization} = this.props;
  82. if (!this.canCreateTeam()) {
  83. return;
  84. }
  85. e.stopPropagation();
  86. e.preventDefault();
  87. openCreateTeamModal({
  88. project,
  89. organization,
  90. onClose: data => {
  91. addTeamToProject(this.api, organization.slug, project.slug, data).then(
  92. this.remountComponent,
  93. this.remountComponent
  94. );
  95. },
  96. });
  97. };
  98. renderBody() {
  99. const {params, organization} = this.props;
  100. const canCreateTeam = this.canCreateTeam();
  101. const hasAccess = organization.access.includes('project:write');
  102. const confirmRemove = t(
  103. 'This is the last team with access to this project. Removing it will mean ' +
  104. 'only organization owners and managers will be able to access the project pages. Are ' +
  105. 'you sure you want to remove this team from the project %s?',
  106. params.projectId
  107. );
  108. const {projectTeams} = this.state;
  109. const menuHeader = (
  110. <StyledTeamsLabel>
  111. {t('Teams')}
  112. <Tooltip
  113. disabled={canCreateTeam}
  114. title={t('You must be a project admin to create teams')}
  115. position="top"
  116. >
  117. <StyledCreateTeamLink
  118. to=""
  119. disabled={!canCreateTeam}
  120. onClick={this.handleCreateTeam}
  121. >
  122. {t('Create Team')}
  123. </StyledCreateTeamLink>
  124. </Tooltip>
  125. </StyledTeamsLabel>
  126. );
  127. return (
  128. <div>
  129. <SettingsPageHeader title={t('%s Teams', params.projectId)} />
  130. <TeamSelect
  131. organization={organization}
  132. selectedTeams={projectTeams ?? []}
  133. onAddTeam={this.handleAdd}
  134. onRemoveTeam={this.handleRemove}
  135. menuHeader={menuHeader}
  136. confirmLastTeamRemoveMessage={confirmRemove}
  137. disabled={!hasAccess}
  138. />
  139. </div>
  140. );
  141. }
  142. }
  143. const StyledTeamsLabel = styled('div')`
  144. font-size: 0.875em;
  145. padding: ${space(0.5)} 0px;
  146. text-transform: uppercase;
  147. `;
  148. const StyledCreateTeamLink = styled(Link)`
  149. float: right;
  150. text-transform: none;
  151. ${p =>
  152. p.disabled &&
  153. css`
  154. cursor: not-allowed;
  155. color: ${p.theme.gray300};
  156. opacity: 0.6;
  157. `};
  158. `;
  159. export default ProjectTeams;