platform.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import {Fragment, useCallback, useContext, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {LocationDescriptorObject} from 'history';
  4. import omit from 'lodash/omit';
  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 {SdkDocumentation} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
  12. import type {ProductSolution} from 'sentry/components/onboarding/productSelection';
  13. import {platformProductAvailability} from 'sentry/components/onboarding/productSelection';
  14. import {setPageFiltersStorage} from 'sentry/components/organizations/pageFilters/persistence';
  15. import {performance as performancePlatforms} from 'sentry/data/platformCategories';
  16. import type {Platform} from 'sentry/data/platformPickerCategories';
  17. import platforms from 'sentry/data/platforms';
  18. import {t} from 'sentry/locale';
  19. import ConfigStore from 'sentry/stores/configStore';
  20. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  21. import {space} from 'sentry/styles/space';
  22. import type {IssueAlertRule} from 'sentry/types/alerts';
  23. import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
  24. import type {PlatformIntegration, PlatformKey, Project} from 'sentry/types/project';
  25. import {trackAnalytics} from 'sentry/utils/analytics';
  26. import {useApiQuery} from 'sentry/utils/queryClient';
  27. import {decodeList} from 'sentry/utils/queryString';
  28. import {useLocation} from 'sentry/utils/useLocation';
  29. import {useNavigate} from 'sentry/utils/useNavigate';
  30. import useOrganization from 'sentry/utils/useOrganization';
  31. import {GettingStartedWithProjectContext} from 'sentry/views/projects/gettingStartedWithProjectContext';
  32. import {OtherPlatformsInfo} from './otherPlatformsInfo';
  33. import {PlatformDocHeader} from './platformDocHeader';
  34. const ProductUnavailableCTAHook = HookOrDefault({
  35. hookName: 'component:product-unavailable-cta',
  36. });
  37. type Props = {
  38. currentPlatformKey: PlatformKey;
  39. loading: boolean;
  40. platform: PlatformIntegration | undefined;
  41. project: Project | undefined;
  42. };
  43. export function ProjectInstallPlatform({
  44. loading,
  45. project,
  46. currentPlatformKey,
  47. platform: currentPlatform,
  48. }: Props) {
  49. const organization = useOrganization();
  50. const location = useLocation();
  51. const navigate = useNavigate();
  52. const gettingStartedWithProjectContext = useContext(GettingStartedWithProjectContext);
  53. const isSelfHosted = ConfigStore.get('isSelfHosted');
  54. const products = useMemo(
  55. () => decodeList(location.query.product ?? []) as ProductSolution[],
  56. [location.query.product]
  57. );
  58. const {
  59. data: projectAlertRules,
  60. isPending: projectAlertRulesIsLoading,
  61. isError: projectAlertRulesIsError,
  62. } = useApiQuery<IssueAlertRule[]>(
  63. [`/projects/${organization.slug}/${project?.slug}/rules/`],
  64. {
  65. enabled: !!project?.slug,
  66. staleTime: 0,
  67. }
  68. );
  69. useEffect(() => {
  70. if (!project || projectAlertRulesIsLoading || projectAlertRulesIsError) {
  71. return;
  72. }
  73. if (gettingStartedWithProjectContext.project?.id === project.id) {
  74. return;
  75. }
  76. const platformKey = Object.keys(platforms).find(
  77. key => platforms[key].id === project.platform
  78. );
  79. if (!platformKey) {
  80. return;
  81. }
  82. gettingStartedWithProjectContext.setProject({
  83. id: project.id,
  84. name: project.name,
  85. // sometimes the team slug here can be undefined
  86. teamSlug: project.team?.slug,
  87. alertRules: projectAlertRules,
  88. platform: {
  89. ...omit(platforms[platformKey], 'id'),
  90. key: platforms[platformKey].id,
  91. } as OnboardingSelectedSDK,
  92. });
  93. }, [
  94. gettingStartedWithProjectContext,
  95. project,
  96. projectAlertRules,
  97. projectAlertRulesIsLoading,
  98. projectAlertRulesIsError,
  99. ]);
  100. const platform: Platform = {
  101. key: currentPlatformKey,
  102. id: currentPlatform?.id,
  103. name: currentPlatform?.name,
  104. link: currentPlatform?.link,
  105. };
  106. const redirectWithProjectSelection = useCallback(
  107. (to: LocationDescriptorObject) => {
  108. if (!project?.id) {
  109. return;
  110. }
  111. // We need to persist and pin the project filter
  112. // so the selection does not reset on further navigation
  113. PageFiltersStore.updateProjects([Number(project?.id)], null);
  114. PageFiltersStore.pin('projects', true);
  115. setPageFiltersStorage(organization.slug, new Set(['projects']));
  116. navigate({
  117. ...to,
  118. query: {
  119. ...to.query,
  120. project: project?.id,
  121. },
  122. });
  123. },
  124. [navigate, organization.slug, project?.id]
  125. );
  126. if (!project) {
  127. return null;
  128. }
  129. if (!platform.id && platform.key !== 'other') {
  130. return <NotFound />;
  131. }
  132. // because we fall back to 'other' this will always be defined
  133. if (!currentPlatform) {
  134. return null;
  135. }
  136. const issueStreamLink = `/organizations/${organization.slug}/issues/`;
  137. const showPerformancePrompt = performancePlatforms.includes(platform.id as PlatformKey);
  138. const isGettingStarted = window.location.href.indexOf('getting-started') > 0;
  139. const showDocsWithProductSelection =
  140. (platformProductAvailability[platform.key] ?? []).length > 0;
  141. return (
  142. <Fragment>
  143. {!isSelfHosted && showDocsWithProductSelection && (
  144. <ProductUnavailableCTAHook organization={organization} />
  145. )}
  146. <PlatformDocHeader projectSlug={project.slug} platform={platform} />
  147. {platform.key === 'other' ? (
  148. <OtherPlatformsInfo
  149. projectSlug={project.slug}
  150. platform={platform.name ?? 'other'}
  151. />
  152. ) : (
  153. <SdkDocumentation
  154. platform={currentPlatform}
  155. organization={organization}
  156. projectSlug={project.slug}
  157. projectId={project.id}
  158. activeProductSelection={products}
  159. />
  160. )}
  161. <div>
  162. {isGettingStarted && showPerformancePrompt && (
  163. <Feature
  164. features="performance-view"
  165. hookName="feature-disabled:performance-new-project"
  166. >
  167. {({hasFeature}) => {
  168. if (hasFeature) {
  169. return null;
  170. }
  171. return (
  172. <StyledAlert type="info" showIcon>
  173. {t(
  174. `Your selected platform supports performance, but your organization does not have performance enabled.`
  175. )}
  176. </StyledAlert>
  177. );
  178. }}
  179. </Feature>
  180. )}
  181. <StyledButtonBar gap={1}>
  182. <Button
  183. priority="primary"
  184. busy={loading}
  185. onClick={() => {
  186. trackAnalytics('onboarding.take_me_to_issues_clicked', {
  187. organization,
  188. platform: platform.name ?? 'unknown',
  189. project_id: project.id,
  190. products,
  191. });
  192. redirectWithProjectSelection({
  193. pathname: issueStreamLink,
  194. hash: '#welcome',
  195. });
  196. }}
  197. >
  198. {t('Take me to Issues')}
  199. </Button>
  200. </StyledButtonBar>
  201. </div>
  202. </Fragment>
  203. );
  204. }
  205. const StyledButtonBar = styled(ButtonBar)`
  206. margin-top: ${space(3)};
  207. width: max-content;
  208. @media (max-width: ${p => p.theme.breakpoints.small}) {
  209. width: auto;
  210. grid-row-gap: ${space(1)};
  211. grid-auto-flow: row;
  212. }
  213. `;
  214. const StyledAlert = styled(Alert)`
  215. margin-top: ${space(2)};
  216. `;