createProjectsFooter.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import {Fragment} 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 {createProject} from 'sentry/actionCreators/projects';
  12. import {Button} from 'sentry/components/button';
  13. import TextOverflow from 'sentry/components/textOverflow';
  14. import {PlatformKey} from 'sentry/data/platformCategories';
  15. import {t, tct, tn} from 'sentry/locale';
  16. import ProjectsStore from 'sentry/stores/projectsStore';
  17. import {space} from 'sentry/styles/space';
  18. import {Organization} from 'sentry/types';
  19. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  20. import getPlatformName from 'sentry/utils/getPlatformName';
  21. import testableTransition from 'sentry/utils/testableTransition';
  22. import useApi from 'sentry/utils/useApi';
  23. import useProjects from 'sentry/utils/useProjects';
  24. import useTeams from 'sentry/utils/useTeams';
  25. import {OnboardingState} from '../types';
  26. import {usePersistedOnboardingState} from '../utils';
  27. import GenericFooter from './genericFooter';
  28. type Props = {
  29. clearPlatforms: () => void;
  30. genSkipOnboardingLink: () => React.ReactNode;
  31. onComplete: (selectedPlatforms: PlatformKey[]) => void;
  32. organization: Organization;
  33. platforms: PlatformKey[];
  34. };
  35. export default function CreateProjectsFooter({
  36. organization,
  37. platforms,
  38. onComplete,
  39. genSkipOnboardingLink,
  40. clearPlatforms,
  41. }: Props) {
  42. const singleSelectPlatform = !!organization?.features.includes(
  43. 'onboarding-remove-multiselect-platform'
  44. );
  45. const api = useApi();
  46. const {teams} = useTeams();
  47. const [clientState, setClientState] = usePersistedOnboardingState();
  48. const {projects} = useProjects();
  49. const createProjects = async () => {
  50. if (!clientState) {
  51. // Do nothing if client state is not loaded yet.
  52. return;
  53. }
  54. const createProjectForPlatforms = platforms
  55. .filter(platform => !clientState.platformToProjectIdMap[platform])
  56. // filter out platforms that already have a project
  57. .filter(platform => !projects.find(p => p.platform === platform));
  58. if (createProjectForPlatforms.length === 0) {
  59. setClientState({
  60. platformToProjectIdMap: clientState.platformToProjectIdMap,
  61. selectedPlatforms: platforms,
  62. state: 'projects_selected',
  63. url: 'setup-docs/',
  64. });
  65. trackAdvancedAnalyticsEvent('growth.onboarding_set_up_your_projects', {
  66. platforms: platforms.join(','),
  67. platform_count: platforms.length,
  68. organization,
  69. });
  70. onComplete(platforms);
  71. return;
  72. }
  73. try {
  74. addLoadingMessage(
  75. singleSelectPlatform ? t('Creating project') : t('Creating projects')
  76. );
  77. const responses = await Promise.all(
  78. createProjectForPlatforms.map(platform =>
  79. createProject(api, organization.slug, teams[0].slug, platform, platform, {
  80. defaultRules: true,
  81. })
  82. )
  83. );
  84. const nextState: OnboardingState = {
  85. platformToProjectIdMap: clientState.platformToProjectIdMap,
  86. selectedPlatforms: platforms,
  87. state: 'projects_selected',
  88. url: 'setup-docs/',
  89. };
  90. responses.forEach(p => (nextState.platformToProjectIdMap[p.platform] = p.slug));
  91. setClientState(nextState);
  92. responses.forEach(data => ProjectsStore.onCreateSuccess(data, organization.slug));
  93. trackAdvancedAnalyticsEvent('growth.onboarding_set_up_your_projects', {
  94. platforms: platforms.join(','),
  95. platform_count: platforms.length,
  96. organization,
  97. });
  98. clearIndicators();
  99. setTimeout(() => onComplete(platforms));
  100. } catch (err) {
  101. addErrorMessage(
  102. singleSelectPlatform
  103. ? t('Failed to create project')
  104. : t('Failed to create projects')
  105. );
  106. Sentry.captureException(err);
  107. }
  108. };
  109. const renderPlatform = (platform: PlatformKey) => {
  110. platform = platform || 'other';
  111. return <SelectedPlatformIcon key={platform} platform={platform} size={23} />;
  112. };
  113. return (
  114. <GenericFooter>
  115. {genSkipOnboardingLink()}
  116. <SelectionWrapper>
  117. {platforms.length ? (
  118. singleSelectPlatform ? (
  119. <Fragment>
  120. <div>{platforms.map(renderPlatform)}</div>
  121. <PlatformSelected>
  122. {tct('[platform] selected', {
  123. platform: (
  124. <PlatformName>
  125. {getPlatformName(platforms[0]) ?? 'other'}
  126. </PlatformName>
  127. ),
  128. })}
  129. <ClearButton priority="link" onClick={clearPlatforms} size="zero">
  130. {t('Clear')}
  131. </ClearButton>
  132. </PlatformSelected>
  133. </Fragment>
  134. ) : (
  135. <Fragment>
  136. <div>{platforms.map(renderPlatform)}</div>
  137. <PlatformsSelected>
  138. {tn('%s platform selected', '%s platforms selected', platforms.length)}
  139. <ClearButton priority="link" onClick={clearPlatforms} size="zero">
  140. {t('Clear')}
  141. </ClearButton>
  142. </PlatformsSelected>
  143. </Fragment>
  144. )
  145. ) : null}
  146. </SelectionWrapper>
  147. <ButtonWrapper>
  148. {singleSelectPlatform ? (
  149. <Button
  150. priority="primary"
  151. onClick={createProjects}
  152. disabled={platforms.length === 0}
  153. data-test-id="platform-select-next"
  154. title={
  155. platforms.length === 0
  156. ? t('Select the platform you want to monitor')
  157. : undefined
  158. }
  159. >
  160. {t('Create Project')}
  161. </Button>
  162. ) : (
  163. <Button
  164. priority="primary"
  165. onClick={createProjects}
  166. disabled={platforms.length === 0}
  167. data-test-id="platform-select-next"
  168. >
  169. {tn('Create Project', 'Create Projects', platforms.length)}
  170. </Button>
  171. )}
  172. </ButtonWrapper>
  173. </GenericFooter>
  174. );
  175. }
  176. const SelectionWrapper = styled(motion.div)`
  177. display: flex;
  178. flex-direction: column;
  179. justify-content: center;
  180. align-items: center;
  181. @media (max-width: ${p => p.theme.breakpoints.small}) {
  182. display: none;
  183. }
  184. `;
  185. SelectionWrapper.defaultProps = {
  186. transition: testableTransition({
  187. duration: 1.8,
  188. }),
  189. };
  190. const ButtonWrapper = styled(motion.div)`
  191. display: flex;
  192. height: 100%;
  193. align-items: center;
  194. margin-right: ${space(4)};
  195. margin-left: ${space(4)};
  196. `;
  197. ButtonWrapper.defaultProps = {
  198. transition: testableTransition({
  199. duration: 1.3,
  200. }),
  201. };
  202. const SelectedPlatformIcon = styled(PlatformIcon)`
  203. margin-right: ${space(1)};
  204. `;
  205. const PlatformsSelected = styled('div')`
  206. margin-top: ${space(1)};
  207. `;
  208. const PlatformSelected = styled('div')`
  209. margin-top: ${space(1)};
  210. display: grid;
  211. grid-template-columns: 1fr max-content max-content;
  212. align-items: center;
  213. `;
  214. const ClearButton = styled(Button)`
  215. margin-left: ${space(2)};
  216. padding: 0;
  217. `;
  218. const PlatformName = styled(TextOverflow)`
  219. margin-right: ${space(0.5)};
  220. `;