index.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import {useCallback} from 'react';
  2. import {addErrorMessage, addLoadingMessage} from 'sentry/actionCreators/indicator';
  3. import CheckboxField from 'sentry/components/forms/fields/checkboxField';
  4. import SelectField from 'sentry/components/forms/fields/selectField';
  5. import TextField from 'sentry/components/forms/fields/textField';
  6. import Form from 'sentry/components/forms/form';
  7. import type {OnSubmitCallback} from 'sentry/components/forms/types';
  8. import NarrowLayout from 'sentry/components/narrowLayout';
  9. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  10. import {t, tct} from 'sentry/locale';
  11. import ConfigStore from 'sentry/stores/configStore';
  12. import type {OrganizationSummary} from 'sentry/types/organization';
  13. import {getRegionChoices, shouldDisplayRegions} from 'sentry/utils/regions';
  14. import useApi from 'sentry/utils/useApi';
  15. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  16. export const DATA_STORAGE_DOCS_LINK =
  17. 'https://docs.sentry.io/product/accounts/choose-your-data-center';
  18. function removeDataStorageLocationFromFormData(
  19. formData: Record<string, any>
  20. ): Record<string, any> {
  21. const shallowFormDataClone = {...formData};
  22. delete shallowFormDataClone.dataStorageLocation;
  23. return shallowFormDataClone;
  24. }
  25. function OrganizationCreate() {
  26. const termsUrl = ConfigStore.get('termsUrl');
  27. const privacyUrl = ConfigStore.get('privacyUrl');
  28. const isSelfHosted = ConfigStore.get('isSelfHosted');
  29. const relocationUrl = normalizeUrl(`/relocation/`);
  30. const regionChoices = getRegionChoices();
  31. const client = useApi();
  32. // This is a trimmed down version of the logic in ApiForm. It validates the
  33. // form data prior to submitting the request, and overrides the request host
  34. // with the selected region's URL if one is provided.
  35. const submitOrganizationCreate: OnSubmitCallback = useCallback(
  36. (data, onSubmitSuccess, onSubmitError, _event, formModel) => {
  37. if (!formModel.validateForm()) {
  38. return;
  39. }
  40. const regionUrl = data.dataStorageLocation;
  41. addLoadingMessage(t('Creating Organization\u2026'));
  42. formModel.setFormSaving();
  43. client.request('/organizations/', {
  44. method: 'POST',
  45. data: removeDataStorageLocationFromFormData(data),
  46. host: regionUrl,
  47. success: onSubmitSuccess,
  48. error: onSubmitError,
  49. });
  50. },
  51. [client]
  52. );
  53. return (
  54. <SentryDocumentTitle title={t('Create Organization')}>
  55. <NarrowLayout showLogout>
  56. <h3>{t('Create a New Organization')}</h3>
  57. <p>
  58. {t(
  59. "Organizations represent the top level in your hierarchy. You'll be able to bundle a collection of teams within an organization as well as give organization-wide permissions to users."
  60. )}
  61. </p>
  62. <Form
  63. initialData={{defaultTeam: true}}
  64. submitLabel={t('Create Organization')}
  65. apiEndpoint="/organizations/"
  66. apiMethod="POST"
  67. onSubmit={submitOrganizationCreate}
  68. onSubmitSuccess={(createdOrg: OrganizationSummary) => {
  69. const hasCustomerDomain = createdOrg?.features.includes('customer-domains');
  70. let nextUrl = normalizeUrl(
  71. `/organizations/${createdOrg.slug}/projects/new/`,
  72. {forceCustomerDomain: hasCustomerDomain}
  73. );
  74. if (hasCustomerDomain) {
  75. nextUrl = `${createdOrg.links.organizationUrl}${nextUrl}`;
  76. }
  77. // redirect to project creation *(BYPASS REACT ROUTER AND FORCE PAGE REFRESH TO GRAB CSRF TOKEN)*
  78. // browserHistory.pushState(null, `/organizations/${data.slug}/projects/new/`);
  79. window.location.assign(nextUrl);
  80. }}
  81. onSubmitError={error => {
  82. addErrorMessage(
  83. error.responseJSON?.detail ?? t('Unable to create organization.')
  84. );
  85. }}
  86. requireChanges
  87. >
  88. <TextField
  89. id="organization-name"
  90. name="name"
  91. label={t('Organization Name')}
  92. placeholder={t('e.g. My Company')}
  93. inline={false}
  94. flexibleControlStateSize
  95. stacked
  96. required
  97. />
  98. {shouldDisplayRegions() && (
  99. <SelectField
  100. name="dataStorageLocation"
  101. label={t('Data Storage Location')}
  102. help={tct(
  103. "Choose where to store your organization's data. Please note, you won't be able to change locations once your organization has been created. [learnMore:Learn More]",
  104. {learnMore: <a href={DATA_STORAGE_DOCS_LINK} />}
  105. )}
  106. choices={regionChoices}
  107. inline={false}
  108. stacked
  109. required
  110. />
  111. )}
  112. {termsUrl && privacyUrl && (
  113. <CheckboxField
  114. name="agreeTerms"
  115. label={tct(
  116. 'I agree to the [termsLink:Terms of Service] and the [privacyLink:Privacy Policy]',
  117. {
  118. termsLink: <a href={termsUrl} />,
  119. privacyLink: <a href={privacyUrl} />,
  120. }
  121. )}
  122. inline={false}
  123. stacked
  124. required
  125. />
  126. )}
  127. {!isSelfHosted && ConfigStore.get('features').has('relocation:enabled') && (
  128. <div>
  129. {tct('[relocationLink:Relocating from self-hosted?]', {
  130. relocationLink: <a href={relocationUrl} />,
  131. })}
  132. </div>
  133. )}
  134. </Form>
  135. </NarrowLayout>
  136. </SentryDocumentTitle>
  137. );
  138. }
  139. export default OrganizationCreate;