projectTeams.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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 only organization owners and managers will be able to access the project pages. Are you sure you want to remove this team from the project %s?',
  104. params.projectId
  105. );
  106. const {projectTeams} = this.state;
  107. const menuHeader = (
  108. <StyledTeamsLabel>
  109. {t('Teams')}
  110. <Tooltip
  111. disabled={canCreateTeam}
  112. title={t('You must be a project admin to create teams')}
  113. position="top"
  114. >
  115. <StyledCreateTeamLink
  116. to=""
  117. disabled={!canCreateTeam}
  118. onClick={this.handleCreateTeam}
  119. >
  120. {t('Create Team')}
  121. </StyledCreateTeamLink>
  122. </Tooltip>
  123. </StyledTeamsLabel>
  124. );
  125. return (
  126. <div>
  127. <SettingsPageHeader title={t('%s Teams', params.projectId)} />
  128. <TeamSelect
  129. organization={organization}
  130. selectedTeams={projectTeams ?? []}
  131. onAddTeam={this.handleAdd}
  132. onRemoveTeam={this.handleRemove}
  133. menuHeader={menuHeader}
  134. confirmLastTeamRemoveMessage={confirmRemove}
  135. disabled={!hasAccess}
  136. />
  137. </div>
  138. );
  139. }
  140. }
  141. const StyledTeamsLabel = styled('div')`
  142. font-size: 0.875em;
  143. padding: ${space(0.5)} 0px;
  144. text-transform: uppercase;
  145. `;
  146. const StyledCreateTeamLink = styled(Link)`
  147. float: right;
  148. text-transform: none;
  149. ${p =>
  150. p.disabled &&
  151. css`
  152. cursor: not-allowed;
  153. color: ${p.theme.gray300};
  154. opacity: 0.6;
  155. `};
  156. `;
  157. export default ProjectTeams;