platform.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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/gettingStartedDoc/types';
  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. // @ts-expect-error TS(7015): Element implicitly has an 'any' type because index... Remove this comment to see the full error message
  78. key => platforms[key].id === project.platform
  79. );
  80. if (!platformKey) {
  81. return;
  82. }
  83. gettingStartedWithProjectContext.setProject({
  84. id: project.id,
  85. name: project.name,
  86. // sometimes the team slug here can be undefined
  87. teamSlug: project.team?.slug,
  88. alertRules: projectAlertRules,
  89. platform: {
  90. // @ts-expect-error TS(7015): Element implicitly has an 'any' type because index... Remove this comment to see the full error message
  91. ...omit(platforms[platformKey], 'id'),
  92. // @ts-expect-error TS(7015): Element implicitly has an 'any' type because index... Remove this comment to see the full error message
  93. key: platforms[platformKey].id,
  94. } as OnboardingSelectedSDK,
  95. });
  96. }, [
  97. gettingStartedWithProjectContext,
  98. project,
  99. projectAlertRules,
  100. projectAlertRulesIsLoading,
  101. projectAlertRulesIsError,
  102. ]);
  103. const platform: Platform = {
  104. key: currentPlatformKey,
  105. id: currentPlatform?.id,
  106. name: currentPlatform?.name,
  107. link: currentPlatform?.link,
  108. };
  109. const redirectWithProjectSelection = useCallback(
  110. (to: LocationDescriptorObject) => {
  111. if (!project?.id) {
  112. return;
  113. }
  114. // We need to persist and pin the project filter
  115. // so the selection does not reset on further navigation
  116. PageFiltersStore.updateProjects([Number(project?.id)], null);
  117. PageFiltersStore.pin('projects', true);
  118. setPageFiltersStorage(organization.slug, new Set(['projects']));
  119. navigate({
  120. ...to,
  121. query: {
  122. ...to.query,
  123. project: project?.id,
  124. },
  125. });
  126. },
  127. [navigate, organization.slug, project?.id]
  128. );
  129. if (!project) {
  130. return null;
  131. }
  132. if (!platform.id && platform.key !== 'other') {
  133. return <NotFound />;
  134. }
  135. // because we fall back to 'other' this will always be defined
  136. if (!currentPlatform) {
  137. return null;
  138. }
  139. const issueStreamLink = `/organizations/${organization.slug}/issues/`;
  140. const showPerformancePrompt = performancePlatforms.includes(platform.id as PlatformKey);
  141. const isGettingStarted = window.location.href.indexOf('getting-started') > 0;
  142. const showDocsWithProductSelection =
  143. (platformProductAvailability[platform.key] ?? []).length > 0;
  144. return (
  145. <Fragment>
  146. {!isSelfHosted && showDocsWithProductSelection && (
  147. <ProductUnavailableCTAHook organization={organization} />
  148. )}
  149. <PlatformDocHeader projectSlug={project.slug} platform={platform} />
  150. {platform.key === 'other' ? (
  151. <OtherPlatformsInfo
  152. projectSlug={project.slug}
  153. platform={platform.name ?? 'other'}
  154. />
  155. ) : (
  156. <SdkDocumentation
  157. platform={currentPlatform}
  158. organization={organization}
  159. projectSlug={project.slug}
  160. projectId={project.id}
  161. activeProductSelection={products}
  162. />
  163. )}
  164. <div>
  165. {isGettingStarted && showPerformancePrompt && (
  166. <Feature
  167. features="performance-view"
  168. hookName="feature-disabled:performance-new-project"
  169. >
  170. {({hasFeature}) => {
  171. if (hasFeature) {
  172. return null;
  173. }
  174. return (
  175. <StyledAlert type="info" showIcon>
  176. {t(
  177. `Your selected platform supports performance, but your organization does not have performance enabled.`
  178. )}
  179. </StyledAlert>
  180. );
  181. }}
  182. </Feature>
  183. )}
  184. <StyledButtonBar gap={1}>
  185. <Button
  186. priority="primary"
  187. busy={loading}
  188. onClick={() => {
  189. trackAnalytics('onboarding.take_me_to_issues_clicked', {
  190. organization,
  191. platform: platform.name ?? 'unknown',
  192. project_id: project.id,
  193. products,
  194. });
  195. redirectWithProjectSelection({
  196. pathname: issueStreamLink,
  197. hash: '#welcome',
  198. });
  199. }}
  200. >
  201. {t('Take me to Issues')}
  202. </Button>
  203. </StyledButtonBar>
  204. </div>
  205. </Fragment>
  206. );
  207. }
  208. const StyledButtonBar = styled(ButtonBar)`
  209. margin-top: ${space(3)};
  210. width: max-content;
  211. @media (max-width: ${p => p.theme.breakpoints.small}) {
  212. width: auto;
  213. grid-row-gap: ${space(1)};
  214. grid-auto-flow: row;
  215. }
  216. `;
  217. const StyledAlert = styled(Alert)`
  218. margin-top: ${space(2)};
  219. `;