createProjectsFooter.tsx 6.4 KB

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