index.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import {lazy, Profiler, Suspense, useCallback, useEffect, useRef} from 'react';
  2. import {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 {initApiClientErrorHandling} from 'sentry/api';
  11. import ErrorBoundary from 'sentry/components/errorBoundary';
  12. import GlobalModal from 'sentry/components/globalModal';
  13. import Indicators from 'sentry/components/indicators';
  14. import {DEPLOY_PREVIEW_CONFIG, EXPERIMENTAL_SPA} from 'sentry/constants';
  15. import AlertStore from 'sentry/stores/alertStore';
  16. import ConfigStore from 'sentry/stores/configStore';
  17. import HookStore from 'sentry/stores/hookStore';
  18. import OrganizationsStore from 'sentry/stores/organizationsStore';
  19. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  20. import isValidOrgSlug from 'sentry/utils/isValidOrgSlug';
  21. import {onRenderCallback} from 'sentry/utils/performanceForSentry';
  22. import useApi from 'sentry/utils/useApi';
  23. import {useColorscheme} from 'sentry/utils/useColorscheme';
  24. import {useHotkeys} from 'sentry/utils/useHotkeys';
  25. import SystemAlerts from './systemAlerts';
  26. type Props = {
  27. children: React.ReactNode;
  28. } & RouteComponentProps<{orgId?: string}, {}>;
  29. const InstallWizard = lazy(() => import('sentry/views/admin/installWizard'));
  30. const NewsletterConsent = lazy(() => import('sentry/views/newsletterConsent'));
  31. /**
  32. * App is the root level container for all uathenticated routes.
  33. */
  34. function App({children, params}: Props) {
  35. useColorscheme();
  36. const api = useApi();
  37. const config = useLegacyStore(ConfigStore);
  38. // Command palette global-shortcut
  39. useHotkeys(
  40. [
  41. {
  42. match: ['command+shift+p', 'command+k', 'ctrl+shift+p', 'ctrl+k'],
  43. includeInputs: true,
  44. callback: () => openCommandPalette(),
  45. },
  46. ],
  47. []
  48. );
  49. // Theme toggle global shortcut
  50. useHotkeys(
  51. [
  52. {
  53. match: ['command+shift+l', 'ctrl+shift+l'],
  54. includeInputs: true,
  55. callback: () =>
  56. ConfigStore.set('theme', config.theme === 'light' ? 'dark' : 'light'),
  57. },
  58. ],
  59. [config.theme]
  60. );
  61. /**
  62. * Loads the users organization list into the OrganizationsStore
  63. */
  64. const loadOrganizations = useCallback(async () => {
  65. try {
  66. const data = await api.requestPromise('/organizations/', {query: {member: '1'}});
  67. OrganizationsStore.load(data);
  68. } catch {
  69. // TODO: do something?
  70. }
  71. }, [api]);
  72. /**
  73. * Creates Alerts for any internal health problems
  74. */
  75. const checkInternalHealth = useCallback(async () => {
  76. let data: any = null;
  77. try {
  78. data = await api.requestPromise('/internal/health/');
  79. } catch {
  80. // TODO: do something?
  81. }
  82. data?.problems?.forEach?.((problem: any) => {
  83. const {id, message, url} = problem;
  84. const type = problem.severity === 'critical' ? 'error' : 'warning';
  85. AlertStore.addAlert({id, message, type, url, opaque: true});
  86. });
  87. }, [api]);
  88. const {sentryUrl} = ConfigStore.get('links');
  89. const {orgId} = params;
  90. const isOrgSlugValid = orgId ? isValidOrgSlug(orgId) : true;
  91. useEffect(() => {
  92. if (orgId === undefined) {
  93. return;
  94. }
  95. if (!isOrgSlugValid) {
  96. window.location.replace(sentryUrl);
  97. return;
  98. }
  99. }, [orgId, sentryUrl, isOrgSlugValid]);
  100. useEffect(() => {
  101. loadOrganizations();
  102. checkInternalHealth();
  103. // Show system-level alerts
  104. config.messages.forEach(msg =>
  105. AlertStore.addAlert({message: msg.message, type: msg.level, neverExpire: true})
  106. );
  107. // The app is running in deploy preview mode
  108. if (DEPLOY_PREVIEW_CONFIG) {
  109. displayDeployPreviewAlert();
  110. }
  111. // The app is running in local SPA mode
  112. if (!DEPLOY_PREVIEW_CONFIG && EXPERIMENTAL_SPA) {
  113. displayExperimentalSpaAlert();
  114. }
  115. // Set the user for analytics
  116. if (config.user) {
  117. HookStore.get('analytics:init-user').map(cb => cb(config.user));
  118. }
  119. initApiClientErrorHandling();
  120. fetchGuides();
  121. // When the app is unloaded clear the organizationst list
  122. return () => OrganizationsStore.load([]);
  123. }, [loadOrganizations, checkInternalHealth, config.messages, config.user]);
  124. function clearUpgrade() {
  125. ConfigStore.set('needsUpgrade', false);
  126. }
  127. function clearNewsletterConsent() {
  128. const flags = {...config.user.flags, newsletter_consent_prompt: false};
  129. ConfigStore.set('user', {...config.user, flags});
  130. }
  131. const needsUpgrade = config.user?.isSuperuser && config.needsUpgrade;
  132. const newsletterConsentPrompt = config.user?.flags?.newsletter_consent_prompt;
  133. function renderBody() {
  134. if (needsUpgrade) {
  135. return (
  136. <Suspense fallback={null}>
  137. <InstallWizard onConfigured={clearUpgrade} />;
  138. </Suspense>
  139. );
  140. }
  141. if (newsletterConsentPrompt) {
  142. return (
  143. <Suspense fallback={null}>
  144. <NewsletterConsent onSubmitSuccess={clearNewsletterConsent} />
  145. </Suspense>
  146. );
  147. }
  148. if (!isOrgSlugValid) {
  149. return null;
  150. }
  151. return children;
  152. }
  153. // Used to restore focus to the container after closing the modal
  154. const mainContainerRef = useRef<HTMLDivElement>(null);
  155. const handleModalClose = useCallback(() => mainContainerRef.current?.focus?.(), []);
  156. return (
  157. <Profiler id="App" onRender={onRenderCallback}>
  158. <MainContainer tabIndex={-1} ref={mainContainerRef}>
  159. <GlobalModal onClose={handleModalClose} />
  160. <SystemAlerts className="messages-container" />
  161. <Indicators className="indicators-container" />
  162. <ErrorBoundary>{renderBody()}</ErrorBoundary>
  163. </MainContainer>
  164. </Profiler>
  165. );
  166. }
  167. export default App;
  168. const MainContainer = styled('div')`
  169. display: flex;
  170. flex-direction: column;
  171. min-height: 100vh;
  172. outline: none;
  173. padding-top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)};
  174. `;