index.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import {useCallback} from 'react';
  2. import {addErrorMessage} 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';
  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. client.request('/organizations/', {
  42. method: 'POST',
  43. data: removeDataStorageLocationFromFormData(data),
  44. host: regionUrl,
  45. success: onSubmitSuccess,
  46. error: onSubmitError,
  47. });
  48. },
  49. [client]
  50. );
  51. return (
  52. <SentryDocumentTitle title={t('Create Organization')}>
  53. <NarrowLayout showLogout>
  54. <h3>{t('Create a New Organization')}</h3>
  55. <p>
  56. {t(
  57. "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."
  58. )}
  59. </p>
  60. <Form
  61. initialData={{defaultTeam: true}}
  62. submitLabel={t('Create Organization')}
  63. apiEndpoint="/organizations/"
  64. apiMethod="POST"
  65. onSubmit={submitOrganizationCreate}
  66. onSubmitSuccess={(createdOrg: OrganizationSummary) => {
  67. const hasCustomerDomain = createdOrg?.features.includes('customer-domains');
  68. let nextUrl = normalizeUrl(
  69. `/organizations/${createdOrg.slug}/projects/new/`,
  70. {forceCustomerDomain: hasCustomerDomain}
  71. );
  72. if (hasCustomerDomain) {
  73. nextUrl = `${createdOrg.links.organizationUrl}${nextUrl}`;
  74. }
  75. // redirect to project creation *(BYPASS REACT ROUTER AND FORCE PAGE REFRESH TO GRAB CSRF TOKEN)*
  76. // browserHistory.pushState(null, `/organizations/${data.slug}/projects/new/`);
  77. window.location.assign(nextUrl);
  78. }}
  79. onSubmitError={error => {
  80. addErrorMessage(
  81. error.responseJSON?.detail ?? t('Unable to create organization.')
  82. );
  83. }}
  84. requireChanges
  85. >
  86. <TextField
  87. id="organization-name"
  88. name="name"
  89. label={t('Organization Name')}
  90. placeholder={t('e.g. My Company')}
  91. inline={false}
  92. flexibleControlStateSize
  93. stacked
  94. required
  95. />
  96. {shouldDisplayRegions() && (
  97. <SelectField
  98. name="dataStorageLocation"
  99. label={t('Data Storage Location')}
  100. help={tct(
  101. "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]",
  102. {learnMore: <a href={DATA_STORAGE_DOCS_LINK} />}
  103. )}
  104. choices={regionChoices}
  105. inline={false}
  106. stacked
  107. required
  108. />
  109. )}
  110. {termsUrl && privacyUrl && (
  111. <CheckboxField
  112. name="agreeTerms"
  113. label={tct(
  114. 'I agree to the [termsLink:Terms of Service] and the [privacyLink:Privacy Policy]',
  115. {
  116. termsLink: <a href={termsUrl} />,
  117. privacyLink: <a href={privacyUrl} />,
  118. }
  119. )}
  120. inline={false}
  121. stacked
  122. required
  123. />
  124. )}
  125. {!isSelfHosted && ConfigStore.get('features').has('relocation:enabled') && (
  126. <div>
  127. {tct('[relocationLink:Relocating from self-hosted?]', {
  128. relocationLink: <a href={relocationUrl} />,
  129. })}
  130. </div>
  131. )}
  132. </Form>
  133. </NarrowLayout>
  134. </SentryDocumentTitle>
  135. );
  136. }
  137. export default OrganizationCreate;