projectTeams.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import type {RouteComponentProps} from 'react-router';
  2. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  3. import {openCreateTeamModal} from 'sentry/actionCreators/modal';
  4. import {addTeamToProject, removeTeamFromProject} from 'sentry/actionCreators/projects';
  5. import {hasEveryAccess} from 'sentry/components/acl/access';
  6. import {t, tct} from 'sentry/locale';
  7. import TeamStore from 'sentry/stores/teamStore';
  8. import type {Organization, Project, Team} from 'sentry/types';
  9. import routeTitleGen from 'sentry/utils/routeTitle';
  10. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  11. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  12. import TeamSelectForProject from 'sentry/views/settings/components/teamSelect/teamSelectForProject';
  13. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  14. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  15. type Props = {
  16. organization: Organization;
  17. project: Project;
  18. } & RouteComponentProps<{projectId: string}, {}>;
  19. type State = {
  20. projectTeams: null | Team[];
  21. } & DeprecatedAsyncView['state'];
  22. class ProjectTeams extends DeprecatedAsyncView<Props, State> {
  23. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  24. const {organization, project} = this.props;
  25. return [['projectTeams', `/projects/${organization.slug}/${project.slug}/teams/`]];
  26. }
  27. getTitle() {
  28. const {projectId} = this.props.params;
  29. return routeTitleGen(t('Project Teams'), projectId, false);
  30. }
  31. canCreateTeam = () => {
  32. const access = this.props.organization.access;
  33. return (
  34. access.includes('org:write') &&
  35. access.includes('team:write') &&
  36. access.includes('project:write')
  37. );
  38. };
  39. handleRemove = (teamSlug: Team['slug']) => {
  40. if (this.state.loading) {
  41. return;
  42. }
  43. const {organization, project} = this.props;
  44. removeTeamFromProject(this.api, organization.slug, project.slug, 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 = (teamSlug: string) => {
  64. if (this.state.loading) {
  65. return;
  66. }
  67. const team = TeamStore.getBySlug(teamSlug);
  68. if (!team) {
  69. addErrorMessage(tct('Unable to find "[teamSlug]"', {teamSlug}));
  70. this.setState({error: true});
  71. return;
  72. }
  73. const {organization, project} = this.props;
  74. addTeamToProject(this.api, organization.slug, project.slug, team).then(
  75. () => {
  76. this.handleAddedTeam(team);
  77. },
  78. () => {
  79. this.setState({
  80. error: true,
  81. loading: false,
  82. });
  83. }
  84. );
  85. };
  86. handleCreateTeam = (e: React.MouseEvent) => {
  87. e.stopPropagation();
  88. e.preventDefault();
  89. const {project, organization} = this.props;
  90. if (!this.canCreateTeam()) {
  91. return;
  92. }
  93. openCreateTeamModal({
  94. project,
  95. organization,
  96. onClose: data => {
  97. addTeamToProject(this.api, organization.slug, project.slug, data).then(
  98. this.remountComponent,
  99. this.remountComponent
  100. );
  101. },
  102. });
  103. };
  104. renderBody() {
  105. const {project, organization} = this.props;
  106. const {projectTeams} = this.state;
  107. const canCreateTeam = this.canCreateTeam();
  108. const hasWriteAccess = hasEveryAccess(['project:write'], {organization, project});
  109. return (
  110. <div>
  111. <SettingsPageHeader title={t('Project Teams for %s', project.slug)} />
  112. <TextBlock>
  113. {t(
  114. 'These teams and their members have access to this project. They can be assigned to issues and alerts created in it.'
  115. )}
  116. </TextBlock>
  117. <TextBlock>
  118. {t(
  119. 'Team Admins can grant other teams access to this project. However, they cannot revoke access unless they are admins for the other teams too.'
  120. )}
  121. </TextBlock>
  122. <PermissionAlert project={project} />
  123. <TeamSelectForProject
  124. disabled={!hasWriteAccess}
  125. canCreateTeam={canCreateTeam}
  126. organization={organization}
  127. project={project}
  128. selectedTeams={projectTeams ?? []}
  129. onAddTeam={this.handleAdd}
  130. onRemoveTeam={this.handleRemove}
  131. onCreateTeam={(team: Team) => {
  132. addTeamToProject(this.api, organization.slug, project.slug, team).then(
  133. this.remountComponent,
  134. this.remountComponent
  135. );
  136. }}
  137. />
  138. </div>
  139. );
  140. }
  141. }
  142. export default ProjectTeams;