platform.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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 = projects.filter(proj => proj.slug === params.projectId)[0];
  108. const heartbeatFooter = !!organization?.features.includes(
  109. 'onboarding-heartbeat-footer'
  110. );
  111. const currentPlatform = params.platform ?? 'other';
  112. const platformIntegration = platforms.find(p => p.id === currentPlatform);
  113. const platform: Platform = {
  114. key: currentPlatform as PlatformKey,
  115. id: platformIntegration?.id,
  116. name: platformIntegration?.name,
  117. link: platformIntegration?.link,
  118. };
  119. const redirectToNeutralDocs = useCallback(() => {
  120. if (!project.slug) {
  121. return;
  122. }
  123. router.push(
  124. normalizeUrl(
  125. `/organizations/${organization.slug}/projects/${project.slug}/getting-started/`
  126. )
  127. );
  128. }, [organization.slug, project?.slug, router]);
  129. useEffect(() => {
  130. // redirect if platform is not known.
  131. if (!platform.key || platform.key === 'other') {
  132. redirectToNeutralDocs();
  133. }
  134. }, [platform.key, redirectToNeutralDocs]);
  135. if (!platform.id) {
  136. return <NotFound />;
  137. }
  138. const issueStreamLink = `/organizations/${organization.slug}/issues/`;
  139. const performanceOverviewLink = `/organizations/${organization.slug}/performance/`;
  140. const gettingStartedLink = `/organizations/${organization.slug}/projects/${params.projectId}/getting-started/`;
  141. const showPerformancePrompt = performancePlatforms.includes(platform.id as PlatformKey);
  142. const isGettingStarted = window.location.href.indexOf('getting-started') > 0;
  143. return (
  144. <Fragment>
  145. <StyledPageHeader>
  146. <h2>{t('Configure %(platform)s SDK', {platform: platform.name})}</h2>
  147. <ButtonBar gap={1}>
  148. <Button
  149. icon={<IconChevron direction="left" size="sm" />}
  150. size="sm"
  151. to={gettingStartedLink}
  152. >
  153. {t('Back')}
  154. </Button>
  155. <Button size="sm" href={platform.link ?? undefined} external>
  156. {t('Full Documentation')}
  157. </Button>
  158. </ButtonBar>
  159. </StyledPageHeader>
  160. <div>
  161. {isSelfHosted ? (
  162. <SetUpGeneralSdkDoc
  163. organization={organization}
  164. projectSlug={project.slug}
  165. platform={platform}
  166. />
  167. ) : (
  168. <SetUpSdkDoc
  169. organization={organization}
  170. project={project}
  171. location={location}
  172. platform={platform}
  173. />
  174. )}
  175. {isGettingStarted && showPerformancePrompt && (
  176. <Feature
  177. features={['performance-view']}
  178. hookName="feature-disabled:performance-new-project"
  179. >
  180. {({hasFeature}) => {
  181. if (hasFeature) {
  182. return null;
  183. }
  184. return (
  185. <StyledAlert type="info" showIcon>
  186. {t(
  187. `Your selected platform supports performance, but your organization does not have performance enabled.`
  188. )}
  189. </StyledAlert>
  190. );
  191. }}
  192. </Feature>
  193. )}
  194. {isGettingStarted && heartbeatFooter ? (
  195. <Footer
  196. projectSlug={params.projectId}
  197. projectId={project?.id}
  198. route={route}
  199. router={router}
  200. location={location}
  201. />
  202. ) : (
  203. <StyledButtonBar gap={1}>
  204. <Button
  205. priority="primary"
  206. busy={loadingProjects}
  207. to={{
  208. pathname: issueStreamLink,
  209. query: project?.id,
  210. hash: '#welcome',
  211. }}
  212. >
  213. {t('Take me to Issues')}
  214. </Button>
  215. <Button
  216. busy={loadingProjects}
  217. to={{
  218. pathname: performanceOverviewLink,
  219. query: project?.id,
  220. }}
  221. >
  222. {t('Take me to Performance')}
  223. </Button>
  224. </StyledButtonBar>
  225. )}
  226. </div>
  227. </Fragment>
  228. );
  229. }
  230. const StyledButtonBar = styled(ButtonBar)`
  231. margin-top: ${space(3)};
  232. width: max-content;
  233. @media (max-width: ${p => p.theme.breakpoints.small}) {
  234. width: auto;
  235. grid-row-gap: ${space(1)};
  236. grid-auto-flow: row;
  237. }
  238. `;
  239. const StyledPageHeader = styled('div')`
  240. display: flex;
  241. justify-content: space-between;
  242. margin-bottom: ${space(3)};
  243. h2 {
  244. margin: 0;
  245. }
  246. @media (max-width: ${p => p.theme.breakpoints.small}) {
  247. flex-direction: column;
  248. align-items: flex-start;
  249. h2 {
  250. margin-bottom: ${space(2)};
  251. }
  252. }
  253. `;
  254. const StyledAlert = styled(Alert)`
  255. margin-top: ${space(2)};
  256. `;