projectGeneralSettings.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import {Component} from 'react';
  2. import {browserHistory, RouteComponentProps} from 'react-router';
  3. import {
  4. changeProjectSlug,
  5. removeProject,
  6. transferProject,
  7. } from 'sentry/actionCreators/projects';
  8. import ProjectActions from 'sentry/actions/projectActions';
  9. import Button from 'sentry/components/button';
  10. import Confirm from 'sentry/components/confirm';
  11. import Field from 'sentry/components/forms/field';
  12. import Form from 'sentry/components/forms/form';
  13. import JsonForm from 'sentry/components/forms/jsonForm';
  14. import {FieldValue} from 'sentry/components/forms/model';
  15. import TextField from 'sentry/components/forms/textField';
  16. import {removePageFiltersStorage} from 'sentry/components/organizations/pageFilters/persistence';
  17. import {Panel, PanelAlert, PanelHeader} from 'sentry/components/panels';
  18. import {fields} from 'sentry/data/forms/projectGeneralSettings';
  19. import {t, tct} from 'sentry/locale';
  20. import ProjectsStore from 'sentry/stores/projectsStore';
  21. import {Organization, Project} from 'sentry/types';
  22. import handleXhrErrorResponse from 'sentry/utils/handleXhrErrorResponse';
  23. import recreateRoute from 'sentry/utils/recreateRoute';
  24. import routeTitleGen from 'sentry/utils/routeTitle';
  25. import withOrganization from 'sentry/utils/withOrganization';
  26. import AsyncView from 'sentry/views/asyncView';
  27. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  28. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  29. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  30. type Props = AsyncView['props'] &
  31. RouteComponentProps<{orgId: string; projectId: string}, {}> & {
  32. onChangeSlug: (slug: string) => void;
  33. organization: Organization;
  34. };
  35. type State = AsyncView['state'] & {
  36. data: Project;
  37. };
  38. class ProjectGeneralSettings extends AsyncView<Props, State> {
  39. private _form: Record<string, FieldValue> = {};
  40. getTitle() {
  41. const {projectId} = this.props.params;
  42. return routeTitleGen(t('Project Settings'), projectId, false);
  43. }
  44. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  45. const {orgId, projectId} = this.props.params;
  46. return [['data', `/projects/${orgId}/${projectId}/`]];
  47. }
  48. handleTransferFieldChange = (id: string, value: FieldValue) => {
  49. this._form[id] = value;
  50. };
  51. handleRemoveProject = () => {
  52. const {orgId} = this.props.params;
  53. const project = this.state.data;
  54. removePageFiltersStorage(orgId);
  55. if (!project) {
  56. return;
  57. }
  58. removeProject(this.api, orgId, project).then(() => {
  59. // Need to hard reload because lots of components do not listen to Projects Store
  60. window.location.assign('/');
  61. }, handleXhrErrorResponse('Unable to remove project'));
  62. };
  63. handleTransferProject = async () => {
  64. const {orgId} = this.props.params;
  65. const project = this.state.data;
  66. if (!project) {
  67. return;
  68. }
  69. if (typeof this._form.email !== 'string' || this._form.email.length < 1) {
  70. return;
  71. }
  72. try {
  73. await transferProject(this.api, orgId, project, this._form.email);
  74. // Need to hard reload because lots of components do not listen to Projects Store
  75. window.location.assign('/');
  76. } catch (err) {
  77. if (err.status >= 500) {
  78. handleXhrErrorResponse('Unable to transfer project')(err);
  79. }
  80. }
  81. };
  82. isProjectAdmin = () => new Set(this.props.organization.access).has('project:admin');
  83. renderRemoveProject() {
  84. const project = this.state.data;
  85. const isProjectAdmin = this.isProjectAdmin();
  86. const {isInternal} = project;
  87. return (
  88. <Field
  89. label={t('Remove Project')}
  90. help={tct(
  91. 'Remove the [project] project and all related data. [linebreak] Careful, this action cannot be undone.',
  92. {
  93. project: <strong>{project.slug}</strong>,
  94. linebreak: <br />,
  95. }
  96. )}
  97. >
  98. {!isProjectAdmin &&
  99. t('You do not have the required permission to remove this project.')}
  100. {isInternal &&
  101. t(
  102. 'This project cannot be removed. It is used internally by the Sentry server.'
  103. )}
  104. {isProjectAdmin && !isInternal && (
  105. <Confirm
  106. onConfirm={this.handleRemoveProject}
  107. priority="danger"
  108. confirmText={t('Remove project')}
  109. message={
  110. <div>
  111. <TextBlock>
  112. <strong>
  113. {t('Removing this project is permanent and cannot be undone!')}
  114. </strong>
  115. </TextBlock>
  116. <TextBlock>
  117. {t('This will also remove all associated event data.')}
  118. </TextBlock>
  119. </div>
  120. }
  121. >
  122. <div>
  123. <Button className="ref-remove-project" type="button" priority="danger">
  124. {t('Remove Project')}
  125. </Button>
  126. </div>
  127. </Confirm>
  128. )}
  129. </Field>
  130. );
  131. }
  132. renderTransferProject() {
  133. const project = this.state.data;
  134. const isProjectAdmin = this.isProjectAdmin();
  135. const {isInternal} = project;
  136. return (
  137. <Field
  138. label={t('Transfer Project')}
  139. help={tct(
  140. 'Transfer the [project] project and all related data. [linebreak] Careful, this action cannot be undone.',
  141. {
  142. project: <strong>{project.slug}</strong>,
  143. linebreak: <br />,
  144. }
  145. )}
  146. >
  147. {!isProjectAdmin &&
  148. t('You do not have the required permission to transfer this project.')}
  149. {isInternal &&
  150. t(
  151. 'This project cannot be transferred. It is used internally by the Sentry server.'
  152. )}
  153. {isProjectAdmin && !isInternal && (
  154. <Confirm
  155. onConfirm={this.handleTransferProject}
  156. priority="danger"
  157. confirmText={t('Transfer project')}
  158. renderMessage={({confirm}) => (
  159. <div>
  160. <TextBlock>
  161. <strong>
  162. {t('Transferring this project is permanent and cannot be undone!')}
  163. </strong>
  164. </TextBlock>
  165. <TextBlock>
  166. {t(
  167. 'Please enter the email of an organization owner to whom you would like to transfer this project.'
  168. )}
  169. </TextBlock>
  170. <Panel>
  171. <Form
  172. hideFooter
  173. onFieldChange={this.handleTransferFieldChange}
  174. onSubmit={(_data, _onSuccess, _onError, e) => {
  175. e.stopPropagation();
  176. confirm();
  177. }}
  178. >
  179. <TextField
  180. name="email"
  181. label={t('Organization Owner')}
  182. placeholder="admin@example.com"
  183. required
  184. help={t(
  185. 'A request will be emailed to this address, asking the organization owner to accept the project transfer.'
  186. )}
  187. />
  188. </Form>
  189. </Panel>
  190. </div>
  191. )}
  192. >
  193. <div>
  194. <Button className="ref-transfer-project" type="button" priority="danger">
  195. {t('Transfer Project')}
  196. </Button>
  197. </div>
  198. </Confirm>
  199. )}
  200. </Field>
  201. );
  202. }
  203. renderBody() {
  204. const {organization} = this.props;
  205. const project = this.state.data;
  206. const {orgId, projectId} = this.props.params;
  207. const endpoint = `/projects/${orgId}/${projectId}/`;
  208. const access = new Set(organization.access);
  209. const jsonFormProps = {
  210. additionalFieldProps: {
  211. organization,
  212. },
  213. features: new Set(organization.features),
  214. access,
  215. disabled: !access.has('project:write'),
  216. };
  217. const team = project.teams.length ? project.teams?.[0] : undefined;
  218. return (
  219. <div>
  220. <SettingsPageHeader title={t('Project Settings')} />
  221. <PermissionAlert />
  222. <Form
  223. saveOnBlur
  224. allowUndo
  225. initialData={{
  226. ...project,
  227. team,
  228. }}
  229. apiMethod="PUT"
  230. apiEndpoint={endpoint}
  231. onSubmitSuccess={resp => {
  232. this.setState({data: resp});
  233. if (projectId !== resp.slug) {
  234. changeProjectSlug(projectId, resp.slug);
  235. // Container will redirect after stores get updated with new slug
  236. this.props.onChangeSlug(resp.slug);
  237. }
  238. // This will update our project context
  239. ProjectActions.updateSuccess(resp);
  240. }}
  241. >
  242. <JsonForm
  243. {...jsonFormProps}
  244. title={t('Project Details')}
  245. fields={[fields.name, fields.platform]}
  246. />
  247. <JsonForm
  248. {...jsonFormProps}
  249. title={t('Email')}
  250. fields={[fields.subjectPrefix]}
  251. />
  252. <JsonForm
  253. {...jsonFormProps}
  254. title={t('Event Settings')}
  255. fields={[fields.resolveAge]}
  256. />
  257. <JsonForm
  258. {...jsonFormProps}
  259. title={t('Client Security')}
  260. fields={[
  261. fields.allowedDomains,
  262. fields.scrapeJavaScript,
  263. fields.securityToken,
  264. fields.securityTokenHeader,
  265. fields.verifySSL,
  266. ]}
  267. renderHeader={() => (
  268. <PanelAlert type="info">
  269. <TextBlock noMargin>
  270. {tct(
  271. 'Configure origin URLs which Sentry should accept events from. This is used for communication with clients like [link].',
  272. {
  273. link: (
  274. <a href="https://github.com/getsentry/sentry-javascript">
  275. sentry-javascript
  276. </a>
  277. ),
  278. }
  279. )}{' '}
  280. {tct(
  281. 'This will restrict requests based on the [Origin] and [Referer] headers.',
  282. {
  283. Origin: <code>Origin</code>,
  284. Referer: <code>Referer</code>,
  285. }
  286. )}
  287. </TextBlock>
  288. </PanelAlert>
  289. )}
  290. />
  291. </Form>
  292. <Panel>
  293. <PanelHeader>{t('Project Administration')}</PanelHeader>
  294. {this.renderRemoveProject()}
  295. {this.renderTransferProject()}
  296. </Panel>
  297. </div>
  298. );
  299. }
  300. }
  301. type ContainerProps = {
  302. organization: Organization;
  303. } & RouteComponentProps<{orgId: string; projectId: string}, {}>;
  304. class ProjectGeneralSettingsContainer extends Component<ContainerProps> {
  305. componentWillUnmount() {
  306. this.unsubscribe();
  307. }
  308. changedSlug: string | undefined = undefined;
  309. unsubscribe = ProjectsStore.listen(() => this.onProjectsUpdate(), undefined);
  310. onProjectsUpdate() {
  311. if (!this.changedSlug) {
  312. return;
  313. }
  314. const project = ProjectsStore.getBySlug(this.changedSlug);
  315. if (!project) {
  316. return;
  317. }
  318. browserHistory.replace(
  319. recreateRoute('', {
  320. ...this.props,
  321. params: {
  322. ...this.props.params,
  323. projectId: this.changedSlug,
  324. },
  325. })
  326. );
  327. }
  328. render() {
  329. return (
  330. <ProjectGeneralSettings
  331. onChangeSlug={(newSlug: string) => (this.changedSlug = newSlug)}
  332. {...this.props}
  333. />
  334. );
  335. }
  336. }
  337. export default withOrganization(ProjectGeneralSettingsContainer);