index.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import {lazy, Suspense, useCallback, useEffect, useRef} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {
  5. displayDeployPreviewAlert,
  6. displayExperimentalSpaAlert,
  7. } from 'sentry/actionCreators/developmentAlerts';
  8. import {fetchGuides} from 'sentry/actionCreators/guides';
  9. import {openCommandPalette} from 'sentry/actionCreators/modal';
  10. import {fetchOrganizations} from 'sentry/actionCreators/organizations';
  11. import {initApiClientErrorHandling} from 'sentry/api';
  12. import ErrorBoundary from 'sentry/components/errorBoundary';
  13. import GlobalModal from 'sentry/components/globalModal';
  14. import Hook from 'sentry/components/hook';
  15. import Indicators from 'sentry/components/indicators';
  16. import {DEPLOY_PREVIEW_CONFIG, EXPERIMENTAL_SPA} from 'sentry/constants';
  17. import AlertStore from 'sentry/stores/alertStore';
  18. import ConfigStore from 'sentry/stores/configStore';
  19. import HookStore from 'sentry/stores/hookStore';
  20. import OrganizationsStore from 'sentry/stores/organizationsStore';
  21. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  22. import isValidOrgSlug from 'sentry/utils/isValidOrgSlug';
  23. import {onRenderCallback, Profiler} from 'sentry/utils/performanceForSentry';
  24. import useApi from 'sentry/utils/useApi';
  25. import {useColorscheme} from 'sentry/utils/useColorscheme';
  26. import {useHotkeys} from 'sentry/utils/useHotkeys';
  27. import {useUser} from 'sentry/utils/useUser';
  28. import type {InstallWizardProps} from 'sentry/views/admin/installWizard';
  29. import {OrganizationContextProvider} from 'sentry/views/organizationContext';
  30. import SystemAlerts from './systemAlerts';
  31. type Props = {
  32. children: React.ReactNode;
  33. } & RouteComponentProps<{orgId?: string}, {}>;
  34. const InstallWizard = lazy(
  35. () => import('sentry/views/admin/installWizard')
  36. // TODO(TS): DeprecatedAsyncComponent prop types are doing something weird
  37. ) as unknown as React.ComponentType<InstallWizardProps>;
  38. const NewsletterConsent = lazy(() => import('sentry/views/newsletterConsent'));
  39. const BeaconConsent = lazy(() => import('sentry/views/beaconConsent'));
  40. /**
  41. * App is the root level container for all uathenticated routes.
  42. */
  43. function App({children, params}: Props) {
  44. useColorscheme();
  45. const api = useApi();
  46. const user = useUser();
  47. const config = useLegacyStore(ConfigStore);
  48. // Command palette global-shortcut
  49. useHotkeys(
  50. [
  51. {
  52. match: ['command+shift+p', 'command+k', 'ctrl+shift+p', 'ctrl+k'],
  53. includeInputs: true,
  54. callback: () => openCommandPalette(),
  55. },
  56. ],
  57. []
  58. );
  59. // Theme toggle global shortcut
  60. useHotkeys(
  61. [
  62. {
  63. match: ['command+shift+l', 'ctrl+shift+l'],
  64. includeInputs: true,
  65. callback: () =>
  66. ConfigStore.set('theme', config.theme === 'light' ? 'dark' : 'light'),
  67. },
  68. ],
  69. [config.theme]
  70. );
  71. /**
  72. * Loads the users organization list into the OrganizationsStore
  73. */
  74. const loadOrganizations = useCallback(async () => {
  75. try {
  76. const data = await fetchOrganizations(api, {member: '1'});
  77. OrganizationsStore.load(data);
  78. } catch {
  79. // TODO: do something?
  80. }
  81. }, [api]);
  82. /**
  83. * Creates Alerts for any internal health problems
  84. */
  85. const checkInternalHealth = useCallback(async () => {
  86. // For saas deployments we have more robust ways of checking application health.
  87. if (!config.isSelfHosted) {
  88. return;
  89. }
  90. let data: any = null;
  91. try {
  92. data = await api.requestPromise('/internal/health/');
  93. } catch {
  94. // TODO: do something?
  95. }
  96. data?.problems?.forEach?.((problem: any) => {
  97. const {id, message, url} = problem;
  98. const type = problem.severity === 'critical' ? 'error' : 'warning';
  99. AlertStore.addAlert({id, message, type, url, opaque: true});
  100. });
  101. }, [api, config.isSelfHosted]);
  102. const {sentryUrl} = ConfigStore.get('links');
  103. const {orgId} = params;
  104. const isOrgSlugValid = orgId ? isValidOrgSlug(orgId) : true;
  105. useEffect(() => {
  106. if (orgId === undefined) {
  107. return;
  108. }
  109. if (!isOrgSlugValid) {
  110. window.location.replace(sentryUrl);
  111. return;
  112. }
  113. }, [orgId, sentryUrl, isOrgSlugValid]);
  114. useEffect(() => {
  115. loadOrganizations();
  116. checkInternalHealth();
  117. // Show system-level alerts
  118. config.messages.forEach(msg =>
  119. AlertStore.addAlert({message: msg.message, type: msg.level, neverExpire: true})
  120. );
  121. // The app is running in deploy preview mode
  122. if (DEPLOY_PREVIEW_CONFIG) {
  123. displayDeployPreviewAlert();
  124. }
  125. // The app is running in local SPA mode
  126. if (!DEPLOY_PREVIEW_CONFIG && EXPERIMENTAL_SPA) {
  127. displayExperimentalSpaAlert();
  128. }
  129. // Set the user for analytics
  130. if (user) {
  131. HookStore.get('analytics:init-user').map(cb => cb(user));
  132. }
  133. initApiClientErrorHandling();
  134. fetchGuides();
  135. // When the app is unloaded clear the organizationst list
  136. return () => OrganizationsStore.load([]);
  137. }, [loadOrganizations, checkInternalHealth, config.messages, user]);
  138. function clearUpgrade() {
  139. ConfigStore.set('needsUpgrade', false);
  140. }
  141. function clearNewsletterConsent() {
  142. const flags = {...user.flags, newsletter_consent_prompt: false};
  143. ConfigStore.set('user', {...user, flags});
  144. }
  145. function clearBeaconConsentPrompt() {
  146. ConfigStore.set('shouldShowBeaconConsentPrompt', false);
  147. }
  148. const displayInstallWizard =
  149. user?.isSuperuser && config.needsUpgrade && config.isSelfHosted;
  150. const newsletterConsentPrompt = user?.flags?.newsletter_consent_prompt;
  151. const partnershipAgreementPrompt = config.partnershipAgreementPrompt;
  152. const beaconConsentPrompt =
  153. user?.isSuperuser && config.isSelfHosted && config.shouldShowBeaconConsentPrompt;
  154. function renderBody() {
  155. if (displayInstallWizard) {
  156. return (
  157. <Suspense fallback={null}>
  158. <InstallWizard onConfigured={clearUpgrade} />
  159. </Suspense>
  160. );
  161. }
  162. if (beaconConsentPrompt) {
  163. return (
  164. <Suspense fallback={null}>
  165. <BeaconConsent onSubmitSuccess={clearBeaconConsentPrompt} />
  166. </Suspense>
  167. );
  168. }
  169. if (partnershipAgreementPrompt) {
  170. return (
  171. <Suspense fallback={null}>
  172. <Hook
  173. name="component:partnership-agreement"
  174. partnerDisplayName={partnershipAgreementPrompt.partnerDisplayName}
  175. agreements={partnershipAgreementPrompt.agreements}
  176. onSubmitSuccess={() => ConfigStore.set('partnershipAgreementPrompt', null)}
  177. organizationSlug={config.customerDomain?.subdomain}
  178. />
  179. </Suspense>
  180. );
  181. }
  182. if (newsletterConsentPrompt) {
  183. return (
  184. <Suspense fallback={null}>
  185. <NewsletterConsent onSubmitSuccess={clearNewsletterConsent} />
  186. </Suspense>
  187. );
  188. }
  189. if (!isOrgSlugValid) {
  190. return null;
  191. }
  192. return children;
  193. }
  194. // Used to restore focus to the container after closing the modal
  195. const mainContainerRef = useRef<HTMLDivElement>(null);
  196. const handleModalClose = useCallback(() => mainContainerRef.current?.focus?.(), []);
  197. return (
  198. <Profiler id="App" onRender={onRenderCallback}>
  199. <OrganizationContextProvider>
  200. <MainContainer tabIndex={-1} ref={mainContainerRef}>
  201. <GlobalModal onClose={handleModalClose} />
  202. <SystemAlerts className="messages-container" />
  203. <Indicators className="indicators-container" />
  204. <ErrorBoundary>{renderBody()}</ErrorBoundary>
  205. </MainContainer>
  206. </OrganizationContextProvider>
  207. </Profiler>
  208. );
  209. }
  210. export default App;
  211. const MainContainer = styled('div')`
  212. display: flex;
  213. flex-direction: column;
  214. min-height: 100vh;
  215. outline: none;
  216. padding-top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)};
  217. `;