import {Component} from 'react'; import {browserHistory, RouteComponentProps} from 'react-router'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import { changeProjectSlug, removeProject, transferProject, } from 'sentry/actionCreators/projects'; import {hasEveryAccess} from 'sentry/components/acl/access'; import {Button} from 'sentry/components/button'; import Confirm from 'sentry/components/confirm'; import FieldGroup from 'sentry/components/forms/fieldGroup'; import TextField from 'sentry/components/forms/fields/textField'; import Form, {FormProps} from 'sentry/components/forms/form'; import JsonForm from 'sentry/components/forms/jsonForm'; import {FieldValue} from 'sentry/components/forms/model'; import Hook from 'sentry/components/hook'; import ExternalLink from 'sentry/components/links/externalLink'; import {removePageFiltersStorage} from 'sentry/components/organizations/pageFilters/persistence'; import {Panel, PanelAlert, PanelHeader} from 'sentry/components/panels'; import {fields} from 'sentry/data/forms/projectGeneralSettings'; import {t, tct} from 'sentry/locale'; import ProjectsStore from 'sentry/stores/projectsStore'; import {Organization, Project} from 'sentry/types'; import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse'; import recreateRoute from 'sentry/utils/recreateRoute'; import RequestError from 'sentry/utils/requestError/requestError'; import routeTitleGen from 'sentry/utils/routeTitle'; import withOrganization from 'sentry/utils/withOrganization'; import AsyncView from 'sentry/views/asyncView'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; import PermissionAlert from 'sentry/views/settings/project/permissionAlert'; type Props = AsyncView['props'] & RouteComponentProps<{projectId: string}, {}> & { onChangeSlug: (slug: string) => void; organization: Organization; }; type State = AsyncView['state'] & { data: Project; }; class ProjectGeneralSettings extends AsyncView { private _form: Record = {}; getTitle() { const {projectId} = this.props.params; return routeTitleGen(t('Project Settings'), projectId, false); } getEndpoints(): ReturnType { const {organization} = this.props; const {projectId} = this.props.params; return [['data', `/projects/${organization.slug}/${projectId}/`]]; } handleTransferFieldChange = (id: string, value: FieldValue) => { this._form[id] = value; }; handleRemoveProject = () => { const {organization} = this.props; const project = this.state.data; removePageFiltersStorage(organization.slug); if (!project) { return; } removeProject({ api: this.api, orgSlug: organization.slug, projectSlug: project.slug, origin: 'settings', }) .then( () => { addSuccessMessage( tct('[project] was successfully removed', {project: project.slug}) ); }, err => { addErrorMessage(tct('Error removing [project]', {project: project.slug})); throw err; } ) .then( () => { // Need to hard reload because lots of components do not listen to Projects Store window.location.assign('/'); }, (err: RequestError) => handleXhrErrorResponse('Unable to remove project', err) ); }; handleTransferProject = async () => { const {organization} = this.props; const project = this.state.data; if (!project) { return; } if (typeof this._form.email !== 'string' || this._form.email.length < 1) { return; } try { await transferProject(this.api, organization.slug, project, this._form.email); // Need to hard reload because lots of components do not listen to Projects Store window.location.assign('/'); } catch (err) { if (err.status >= 500) { handleXhrErrorResponse('Unable to transfer project', err); } } }; isProjectAdmin = () => hasEveryAccess(['project:admin'], { organization: this.props.organization, project: this.state.data, }); renderRemoveProject() { const project = this.state.data; const isProjectAdmin = this.isProjectAdmin(); const {isInternal} = project; return ( {project.slug}, linebreak:
, } )} > {!isProjectAdmin && t('You do not have the required permission to remove this project.')} {isInternal && t( 'This project cannot be removed. It is used internally by the Sentry server.' )} {isProjectAdmin && !isInternal && ( {t('Removing this project is permanent and cannot be undone!')} {t('This will also remove all associated event data.')} } >
)}
); } renderTransferProject() { const project = this.state.data; const isProjectAdmin = this.isProjectAdmin(); const {isInternal} = project; return ( {project.slug}, linebreak:
, } )} > {!isProjectAdmin && t('You do not have the required permission to transfer this project.')} {isInternal && t( 'This project cannot be transferred. It is used internally by the Sentry server.' )} {isProjectAdmin && !isInternal && ( (
{t('Transferring this project is permanent and cannot be undone!')} {t( 'Please enter the email of an organization owner to whom you would like to transfer this project.' )}
{ e.stopPropagation(); confirm(); }} >
)} >
)}
); } renderBody() { const {organization} = this.props; const project = this.state.data; const {projectId} = this.props.params; const endpoint = `/projects/${organization.slug}/${projectId}/`; const access = new Set(organization.access.concat(project.access)); const jsonFormProps = { additionalFieldProps: { organization, }, features: new Set(organization.features), access, disabled: !hasEveryAccess(['project:write'], {organization, project}), }; const team = project.teams.length ? project.teams?.[0] : undefined; /* HACK: The
component applies its props to its children meaning the hooked component would need to conform to the form settings applied in a separate repository. This is not feasible to maintain and may introduce compatability errors if something changes in either repository. For that reason, the Form component is split in two, since the fields do not depend on one another, allowing for the Hook to manage it's own state. */ const formProps: FormProps = { saveOnBlur: true, allowUndo: true, initialData: { ...project, team, }, apiMethod: 'PUT' as const, apiEndpoint: endpoint, onSubmitSuccess: resp => { this.setState({data: resp}); if (projectId !== resp.slug) { changeProjectSlug(projectId, resp.slug); // Container will redirect after stores get updated with new slug this.props.onChangeSlug(resp.slug); } // This will update our project context ProjectsStore.onUpdateSuccess(resp); }, }; return (
( {tct( 'Configure origin URLs which Sentry should accept events from. This is used for communication with clients like [link].', { link: ( sentry-javascript ), } )}{' '} {tct( 'This will restrict requests based on the [Origin] and [Referer] headers.', { Origin: Origin, Referer: Referer, } )} )} /> {t('Project Administration')} {this.renderRemoveProject()} {this.renderTransferProject()}
); } } type ContainerProps = { organization: Organization; } & RouteComponentProps<{projectId: string}, {}>; class ProjectGeneralSettingsContainer extends Component { componentWillUnmount() { this.unsubscribe(); } changedSlug: string | undefined = undefined; unsubscribe = ProjectsStore.listen(() => this.onProjectsUpdate(), undefined); onProjectsUpdate() { if (!this.changedSlug) { return; } const project = ProjectsStore.getBySlug(this.changedSlug); if (!project) { return; } browserHistory.replace( recreateRoute('', { ...this.props, params: { ...this.props.params, projectId: this.changedSlug, }, }) ); } render() { return ( (this.changedSlug = newSlug)} {...this.props} /> ); } } export default withOrganization(ProjectGeneralSettingsContainer);