index.tsx 6.9 KB

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