createProjectsFooter.tsx 7.7 KB

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