platform.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {loadDocs} from 'sentry/actionCreators/projects';
  5. import Feature from 'sentry/components/acl/feature';
  6. import {Alert} from 'sentry/components/alert';
  7. import {Button} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import NotFound from 'sentry/components/errors/notFound';
  10. import HookOrDefault from 'sentry/components/hookOrDefault';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import LoadingError from 'sentry/components/loadingError';
  13. import LoadingIndicator from 'sentry/components/loadingIndicator';
  14. import {DocumentationWrapper} from 'sentry/components/onboarding/documentationWrapper';
  15. import {Footer} from 'sentry/components/onboarding/footer';
  16. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  17. import {
  18. performance as performancePlatforms,
  19. Platform,
  20. PlatformKey,
  21. } from 'sentry/data/platformCategories';
  22. import platforms from 'sentry/data/platforms';
  23. import {IconChevron} from 'sentry/icons';
  24. import {t, tct} from 'sentry/locale';
  25. import ConfigStore from 'sentry/stores/configStore';
  26. import {space} from 'sentry/styles/space';
  27. import {Organization, Project} from 'sentry/types';
  28. import useApi from 'sentry/utils/useApi';
  29. import useOrganization from 'sentry/utils/useOrganization';
  30. import useProjects from 'sentry/utils/useProjects';
  31. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  32. // in this case, the default is rendered inside the hook
  33. const SetUpSdkDoc = HookOrDefault({
  34. hookName: 'component:set-up-sdk-doc',
  35. });
  36. type Props = RouteComponentProps<{platform: string; projectId: string}, {}>;
  37. export function SetUpGeneralSdkDoc({
  38. organization,
  39. projectSlug,
  40. platform,
  41. }: {
  42. organization: Organization;
  43. platform: Platform;
  44. projectSlug: Project['slug'];
  45. }) {
  46. const api = useApi();
  47. const [loading, setLoading] = useState(true);
  48. const [error, setError] = useState(false);
  49. const [html, setHtml] = useState('');
  50. const fetchDocs = useCallback(async () => {
  51. setLoading(true);
  52. try {
  53. const {html: reponse} = await loadDocs({
  54. api,
  55. orgSlug: organization.slug,
  56. projectSlug,
  57. platform: platform.key as PlatformKey,
  58. });
  59. setHtml(reponse);
  60. window.scrollTo(0, 0);
  61. } catch (err) {
  62. setError(err);
  63. }
  64. setLoading(false);
  65. }, [api, organization.slug, projectSlug, platform.key]);
  66. useEffect(() => {
  67. fetchDocs();
  68. }, [fetchDocs]);
  69. return (
  70. <div>
  71. <Alert type="info" showIcon>
  72. {tct(
  73. `
  74. This is a quick getting started guide. For in-depth instructions
  75. on integrating Sentry with [platform], view
  76. [docLink:our complete documentation].`,
  77. {
  78. platform: platform.name,
  79. docLink: <ExternalLink href={platform.link ?? undefined} />,
  80. }
  81. )}
  82. </Alert>
  83. {loading ? (
  84. <LoadingIndicator />
  85. ) : error ? (
  86. <LoadingError onRetry={fetchDocs} />
  87. ) : (
  88. <Fragment>
  89. <SentryDocumentTitle
  90. title={`${t('Configure')} ${platform.name}`}
  91. projectSlug={projectSlug}
  92. />
  93. <DocumentationWrapper dangerouslySetInnerHTML={{__html: html}} />
  94. </Fragment>
  95. )}
  96. </div>
  97. );
  98. }
  99. export function ProjectInstallPlatform({location, params, route, router}: Props) {
  100. const organization = useOrganization();
  101. const isSelfHosted = ConfigStore.get('isSelfHosted');
  102. const {projects, initiallyLoaded} = useProjects({
  103. slugs: [params.projectId],
  104. orgId: organization.slug,
  105. });
  106. const loadingProjects = !initiallyLoaded;
  107. const project = !loadingProjects
  108. ? projects.find(proj => proj.slug === params.projectId)
  109. : undefined;
  110. const heartbeatFooter = !!organization?.features.includes(
  111. 'onboarding-heartbeat-footer'
  112. );
  113. const currentPlatform = params.platform ?? 'other';
  114. const platformIntegration = platforms.find(p => p.id === currentPlatform);
  115. const platform: Platform = {
  116. key: currentPlatform as PlatformKey,
  117. id: platformIntegration?.id,
  118. name: platformIntegration?.name,
  119. link: platformIntegration?.link,
  120. };
  121. const redirectToNeutralDocs = useCallback(() => {
  122. if (!project?.slug) {
  123. return;
  124. }
  125. router.push(
  126. normalizeUrl(
  127. `/organizations/${organization.slug}/projects/${project.slug}/getting-started/`
  128. )
  129. );
  130. }, [organization.slug, project?.slug, router]);
  131. useEffect(() => {
  132. // redirect if platform is not known.
  133. if (!platform.key || platform.key === 'other') {
  134. redirectToNeutralDocs();
  135. }
  136. }, [platform.key, redirectToNeutralDocs]);
  137. if (!platform.id) {
  138. return <NotFound />;
  139. }
  140. const issueStreamLink = `/organizations/${organization.slug}/issues/`;
  141. const performanceOverviewLink = `/organizations/${organization.slug}/performance/`;
  142. const gettingStartedLink = `/organizations/${organization.slug}/projects/${params.projectId}/getting-started/`;
  143. const showPerformancePrompt = performancePlatforms.includes(platform.id as PlatformKey);
  144. const isGettingStarted = window.location.href.indexOf('getting-started') > 0;
  145. if (!project) {
  146. return null;
  147. }
  148. return (
  149. <Fragment>
  150. <StyledPageHeader>
  151. <h2>{t('Configure %(platform)s SDK', {platform: platform.name})}</h2>
  152. <ButtonBar gap={1}>
  153. <Button
  154. icon={<IconChevron direction="left" size="sm" />}
  155. size="sm"
  156. to={gettingStartedLink}
  157. >
  158. {t('Back')}
  159. </Button>
  160. <Button size="sm" href={platform.link ?? undefined} external>
  161. {t('Full Documentation')}
  162. </Button>
  163. </ButtonBar>
  164. </StyledPageHeader>
  165. <div>
  166. {isSelfHosted ? (
  167. <SetUpGeneralSdkDoc
  168. organization={organization}
  169. projectSlug={project.slug}
  170. platform={platform}
  171. />
  172. ) : (
  173. <SetUpSdkDoc
  174. organization={organization}
  175. project={project}
  176. location={location}
  177. platform={platform}
  178. />
  179. )}
  180. {isGettingStarted && showPerformancePrompt && (
  181. <Feature
  182. features={['performance-view']}
  183. hookName="feature-disabled:performance-new-project"
  184. >
  185. {({hasFeature}) => {
  186. if (hasFeature) {
  187. return null;
  188. }
  189. return (
  190. <StyledAlert type="info" showIcon>
  191. {t(
  192. `Your selected platform supports performance, but your organization does not have performance enabled.`
  193. )}
  194. </StyledAlert>
  195. );
  196. }}
  197. </Feature>
  198. )}
  199. {isGettingStarted && heartbeatFooter ? (
  200. <Footer
  201. projectSlug={params.projectId}
  202. projectId={project?.id}
  203. route={route}
  204. router={router}
  205. location={location}
  206. />
  207. ) : (
  208. <StyledButtonBar gap={1}>
  209. <Button
  210. priority="primary"
  211. busy={loadingProjects}
  212. to={{
  213. pathname: issueStreamLink,
  214. query: project?.id,
  215. hash: '#welcome',
  216. }}
  217. >
  218. {t('Take me to Issues')}
  219. </Button>
  220. <Button
  221. busy={loadingProjects}
  222. to={{
  223. pathname: performanceOverviewLink,
  224. query: project?.id,
  225. }}
  226. >
  227. {t('Take me to Performance')}
  228. </Button>
  229. </StyledButtonBar>
  230. )}
  231. </div>
  232. </Fragment>
  233. );
  234. }
  235. const StyledButtonBar = styled(ButtonBar)`
  236. margin-top: ${space(3)};
  237. width: max-content;
  238. @media (max-width: ${p => p.theme.breakpoints.small}) {
  239. width: auto;
  240. grid-row-gap: ${space(1)};
  241. grid-auto-flow: row;
  242. }
  243. `;
  244. const StyledPageHeader = styled('div')`
  245. display: flex;
  246. justify-content: space-between;
  247. margin-bottom: ${space(3)};
  248. h2 {
  249. margin: 0;
  250. }
  251. @media (max-width: ${p => p.theme.breakpoints.small}) {
  252. flex-direction: column;
  253. align-items: flex-start;
  254. h2 {
  255. margin-bottom: ${space(2)};
  256. }
  257. }
  258. `;
  259. const StyledAlert = styled(Alert)`
  260. margin-top: ${space(2)};
  261. `;