createProjectsFooter.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {Fragment, useCallback, useContext} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {motion} from 'framer-motion';
  5. import {PlatformIcon} from 'platformicons';
  6. import {
  7. addErrorMessage,
  8. addLoadingMessage,
  9. clearIndicators,
  10. } from 'sentry/actionCreators/indicator';
  11. import {openModal} from 'sentry/actionCreators/modal';
  12. import {createProject} from 'sentry/actionCreators/projects';
  13. import {Button} from 'sentry/components/button';
  14. import {SUPPORTED_LANGUAGES} from 'sentry/components/onboarding/frameworkSuggestionModal';
  15. import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
  16. import {t} from 'sentry/locale';
  17. import ProjectsStore from 'sentry/stores/projectsStore';
  18. import {space} from 'sentry/styles/space';
  19. import {
  20. OnboardingProjectStatus,
  21. OnboardingSelectedSDK,
  22. Organization,
  23. Project,
  24. } from 'sentry/types';
  25. import {trackAnalytics} from 'sentry/utils/analytics';
  26. import testableTransition from 'sentry/utils/testableTransition';
  27. import useApi from 'sentry/utils/useApi';
  28. import useProjects from 'sentry/utils/useProjects';
  29. import {useTeams} from 'sentry/utils/useTeams';
  30. import GenericFooter from './genericFooter';
  31. type Props = {
  32. clearPlatform: () => void;
  33. genSkipOnboardingLink: () => React.ReactNode;
  34. onComplete: (selectedPlatform: OnboardingSelectedSDK) => void;
  35. organization: Organization;
  36. selectedPlatform?: OnboardingSelectedSDK;
  37. };
  38. export function CreateProjectsFooter({
  39. organization,
  40. selectedPlatform,
  41. onComplete,
  42. genSkipOnboardingLink,
  43. clearPlatform,
  44. }: Props) {
  45. const frameworkSelectionEnabled = !!organization?.features.includes(
  46. 'onboarding-sdk-selection'
  47. );
  48. const api = useApi();
  49. const {teams} = useTeams();
  50. const onboardingContext = useContext(OnboardingContext);
  51. const {projects} = useProjects();
  52. const createPlatformProject = useCallback(
  53. async (selectedFramework?: OnboardingSelectedSDK) => {
  54. if (!selectedPlatform) {
  55. return;
  56. }
  57. let createProjectForPlatform: OnboardingSelectedSDK | undefined = undefined;
  58. if (selectedFramework) {
  59. createProjectForPlatform = projects.find(
  60. p => p.platform === selectedFramework.key
  61. )
  62. ? undefined
  63. : selectedFramework;
  64. } else {
  65. createProjectForPlatform = projects.find(
  66. p => p.platform === onboardingContext.data.selectedSDK?.key
  67. )
  68. ? undefined
  69. : onboardingContext.data.selectedSDK;
  70. }
  71. if (!createProjectForPlatform) {
  72. const platform = selectedFramework ? selectedFramework : selectedPlatform;
  73. trackAnalytics('growth.onboarding_set_up_your_project', {
  74. platform: selectedPlatform.key,
  75. organization,
  76. });
  77. onComplete(platform);
  78. return;
  79. }
  80. try {
  81. addLoadingMessage(t('Loading SDK configuration\u2026'));
  82. const response = (await createProject({
  83. api,
  84. orgSlug: organization.slug,
  85. team: teams[0].slug,
  86. platform: createProjectForPlatform.key,
  87. name: createProjectForPlatform.key,
  88. options: {
  89. defaultRules: true,
  90. },
  91. })) as Project;
  92. ProjectsStore.onCreateSuccess(response, organization.slug);
  93. // Measure to filter out projects that might have been created during the onboarding and not deleted from the session due to an error
  94. // Note: in the onboarding flow the projects are created based on the platform slug
  95. const newProjects = Object.keys(onboardingContext.data.projects).reduce(
  96. (acc, key) => {
  97. if (onboardingContext.data.projects[key].slug !== response.slug) {
  98. acc[key] = onboardingContext.data.projects[key];
  99. }
  100. return acc;
  101. },
  102. {}
  103. );
  104. onboardingContext.setData({
  105. selectedSDK: createProjectForPlatform,
  106. projects: {
  107. ...newProjects,
  108. [response.id]: {
  109. slug: response.slug,
  110. status: OnboardingProjectStatus.WAITING,
  111. },
  112. },
  113. });
  114. trackAnalytics('growth.onboarding_set_up_your_project', {
  115. platform: selectedPlatform.key,
  116. organization,
  117. });
  118. clearIndicators();
  119. setTimeout(() => onComplete(createProjectForPlatform!));
  120. } catch (err) {
  121. addErrorMessage(t('Failed to load SDK configuration'));
  122. Sentry.captureException(err);
  123. }
  124. },
  125. [onboardingContext, selectedPlatform, api, organization, teams, projects, onComplete]
  126. );
  127. const handleProjectCreation = useCallback(async () => {
  128. if (!selectedPlatform) {
  129. return;
  130. }
  131. if (
  132. selectedPlatform.type !== 'language' ||
  133. !Object.values(SUPPORTED_LANGUAGES).includes(
  134. selectedPlatform.language as SUPPORTED_LANGUAGES
  135. )
  136. ) {
  137. createPlatformProject();
  138. return;
  139. }
  140. const {FrameworkSuggestionModal, modalCss} = await import(
  141. 'sentry/components/onboarding/frameworkSuggestionModal'
  142. );
  143. openModal(
  144. deps => (
  145. <FrameworkSuggestionModal
  146. {...deps}
  147. organization={organization}
  148. selectedPlatform={selectedPlatform}
  149. onConfigure={selectedFramework => {
  150. onboardingContext.setData({
  151. ...onboardingContext.data,
  152. selectedSDK: selectedFramework,
  153. });
  154. createPlatformProject(selectedFramework);
  155. }}
  156. onSkip={createPlatformProject}
  157. newOrg
  158. />
  159. ),
  160. {
  161. modalCss,
  162. onClose: () => {
  163. trackAnalytics('onboarding.select_framework_modal_close_button_clicked', {
  164. platform: selectedPlatform.key,
  165. organization,
  166. });
  167. },
  168. }
  169. );
  170. }, [selectedPlatform, createPlatformProject, onboardingContext, organization]);
  171. return (
  172. <GenericFooter>
  173. {genSkipOnboardingLink()}
  174. <SelectionWrapper>
  175. {selectedPlatform ? (
  176. <Fragment>
  177. <div>
  178. <SelectedPlatformIcon
  179. platform={selectedPlatform.key ?? 'other'}
  180. size={23}
  181. />
  182. </div>
  183. <PlatformsSelected>
  184. {t('platform selected')}
  185. <ClearButton priority="link" onClick={clearPlatform} size="zero">
  186. {t('Clear')}
  187. </ClearButton>
  188. </PlatformsSelected>
  189. </Fragment>
  190. ) : null}
  191. </SelectionWrapper>
  192. <ButtonWrapper>
  193. <Button
  194. priority="primary"
  195. onClick={() =>
  196. frameworkSelectionEnabled ? handleProjectCreation() : createPlatformProject()
  197. }
  198. disabled={!selectedPlatform}
  199. data-test-id="platform-select-next"
  200. >
  201. {t('Configure SDK')}
  202. </Button>
  203. </ButtonWrapper>
  204. </GenericFooter>
  205. );
  206. }
  207. const SelectionWrapper = styled(motion.div)`
  208. display: flex;
  209. flex-direction: column;
  210. justify-content: center;
  211. align-items: center;
  212. @media (max-width: ${p => p.theme.breakpoints.small}) {
  213. display: none;
  214. }
  215. `;
  216. SelectionWrapper.defaultProps = {
  217. transition: testableTransition({
  218. duration: 1.8,
  219. }),
  220. };
  221. const ButtonWrapper = styled(motion.div)`
  222. display: flex;
  223. height: 100%;
  224. align-items: center;
  225. margin-right: ${space(4)};
  226. margin-left: ${space(4)};
  227. `;
  228. ButtonWrapper.defaultProps = {
  229. transition: testableTransition({
  230. duration: 1.3,
  231. }),
  232. };
  233. const SelectedPlatformIcon = styled(PlatformIcon)`
  234. margin-right: ${space(1)};
  235. `;
  236. const PlatformsSelected = styled('div')`
  237. margin-top: ${space(1)};
  238. `;
  239. const ClearButton = styled(Button)`
  240. margin-left: ${space(2)};
  241. padding: 0;
  242. `;