createProjectsFooter.tsx 6.4 KB

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