platform.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {Component} from 'react';
  2. import {motion} from 'framer-motion';
  3. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  4. import {createProject} from 'sentry/actionCreators/projects';
  5. import ProjectActions from 'sentry/actions/projectActions';
  6. import {Client} from 'sentry/api';
  7. import Button from 'sentry/components/button';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import PlatformPicker from 'sentry/components/platformPicker';
  10. import {PlatformKey} from 'sentry/data/platformCategories';
  11. import {t, tct} from 'sentry/locale';
  12. import {Team} from 'sentry/types';
  13. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  14. import testableTransition from 'sentry/utils/testableTransition';
  15. import withApi from 'sentry/utils/withApi';
  16. import withTeams from 'sentry/utils/withTeams';
  17. import StepHeading from './components/stepHeading';
  18. import {StepProps} from './types';
  19. type Props = StepProps & {
  20. api: Client;
  21. teams: Team[];
  22. };
  23. type State = {
  24. /**
  25. * This will be flipped to true immediately before creating the first
  26. * project. We use state here to avoid the intermittent prop value where
  27. * the project is created but the store hasn't propagated its value to the
  28. * component yet, leaving a brief period where the button will flash
  29. * between labels / disabled states.
  30. */
  31. firstProjectCreated: boolean;
  32. /**
  33. * `progressing` indicates that we are moving to the next step. Again, this
  34. * is kept as state to avoid intermittent states causing flickering of the
  35. * button.
  36. */
  37. progressing: boolean;
  38. };
  39. class OnboardingPlatform extends Component<Props, State> {
  40. state: State = {
  41. firstProjectCreated: false,
  42. progressing: false,
  43. };
  44. componentDidMount() {
  45. trackAdvancedAnalyticsEvent('growth.onboarding_load_choose_platform', {
  46. organization: this.props.organization ?? null,
  47. });
  48. }
  49. componentDidUpdate(prevProps: Props) {
  50. if (prevProps.active && !this.props.active) {
  51. // eslint-disable-next-line react/no-did-update-set-state
  52. this.setState({progressing: false});
  53. }
  54. }
  55. get hasFirstProject() {
  56. return this.props.project || this.state.firstProjectCreated;
  57. }
  58. get continueButtonLabel() {
  59. if (this.state.progressing) {
  60. return t('Creating Project...');
  61. }
  62. if (!this.hasFirstProject) {
  63. return t('Create Project');
  64. }
  65. if (!this.props.active) {
  66. return t('Project Created');
  67. }
  68. return t('Set Up Your Project');
  69. }
  70. async createFirstProject(platform: PlatformKey) {
  71. const {api, orgId, teams} = this.props;
  72. if (this.hasFirstProject) {
  73. return;
  74. }
  75. if (teams.length < 1) {
  76. return;
  77. }
  78. this.setState({firstProjectCreated: true});
  79. try {
  80. const data = await createProject(api, orgId, teams[0].slug, orgId, platform, {
  81. defaultRules: false,
  82. });
  83. ProjectActions.createSuccess(data);
  84. } catch (error) {
  85. addErrorMessage(t('Failed to create project'));
  86. throw error;
  87. }
  88. }
  89. handleSetPlatform = (platform: PlatformKey | null) => this.props.onUpdate({platform});
  90. handleContinue = async () => {
  91. this.setState({progressing: true});
  92. const {platform} = this.props;
  93. if (platform === null) {
  94. return;
  95. }
  96. trackAdvancedAnalyticsEvent('growth.onboarding_set_up_your_project', {
  97. platform,
  98. organization: this.props.organization ?? null,
  99. });
  100. // Create their first project if they don't already have one. This is a
  101. // no-op if they already have a project.
  102. await this.createFirstProject(platform);
  103. this.props.onComplete({});
  104. };
  105. render() {
  106. const {active, project, platform} = this.props;
  107. const selectedPlatform = platform || (project && project.platform);
  108. const continueDisabled = this.state.progressing || (this.hasFirstProject && !active);
  109. return (
  110. <div>
  111. <StepHeading step={1}>Choose your project’s platform</StepHeading>
  112. <motion.div
  113. transition={testableTransition()}
  114. variants={{
  115. initial: {y: 30, opacity: 0},
  116. animate: {y: 0, opacity: 1},
  117. exit: {opacity: 0},
  118. }}
  119. >
  120. <p>
  121. {tct(
  122. `Variety is the spice of application monitoring. Sentry SDKs integrate
  123. with most languages and platforms your developer heart desires.
  124. [link:View the full list].`,
  125. {link: <ExternalLink href="https://docs.sentry.io/platforms/" />}
  126. )}
  127. </p>
  128. <PlatformPicker
  129. noAutoFilter
  130. platform={selectedPlatform}
  131. setPlatform={this.handleSetPlatform}
  132. source="Onboarding"
  133. organization={this.props.organization}
  134. />
  135. <p>
  136. {tct(
  137. `Don't see your platform-of-choice? Fear not. Select
  138. [otherPlatformLink:other platform] when using a [communityClient:community client].
  139. Need help? Learn more in [docs:our docs].`,
  140. {
  141. otherPlatformLink: (
  142. <Button
  143. priority="link"
  144. onClick={() => this.handleSetPlatform('other')}
  145. aria-label={t('Other platform')}
  146. />
  147. ),
  148. communityClient: (
  149. <ExternalLink href="https://docs.sentry.io/platforms/#community-supported" />
  150. ),
  151. docs: <ExternalLink href="https://docs.sentry.io/platforms/" />,
  152. }
  153. )}
  154. </p>
  155. {selectedPlatform && (
  156. <Button
  157. data-test-id="platform-select-next"
  158. priority="primary"
  159. disabled={continueDisabled}
  160. onClick={this.handleContinue}
  161. >
  162. {this.continueButtonLabel}
  163. </Button>
  164. )}
  165. </motion.div>
  166. </div>
  167. );
  168. }
  169. }
  170. // TODO(davidenwang): change to functional component and replace withTeams with useTeams
  171. export default withApi(withTeams(OnboardingPlatform));