firstEventFooter.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {motion, Variants} from 'framer-motion';
  4. import {Button} from 'sentry/components/button';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import Link from 'sentry/components/links/link';
  7. import {IconCheckmark} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
  10. import {space} from 'sentry/styles/space';
  11. import {Group, Organization, Project} from 'sentry/types';
  12. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  13. import EventWaiter from 'sentry/utils/eventWaiter';
  14. import testableTransition from 'sentry/utils/testableTransition';
  15. import CreateSampleEventButton from 'sentry/views/onboarding/createSampleEventButton';
  16. import {usePersistedOnboardingState} from '../utils';
  17. import GenericFooter from './genericFooter';
  18. interface FirstEventFooterProps {
  19. handleFirstIssueReceived: () => void;
  20. hasFirstEvent: boolean;
  21. isLast: boolean;
  22. onClickSetupLater: () => void;
  23. organization: Organization;
  24. project: Project;
  25. }
  26. export default function FirstEventFooter({
  27. organization,
  28. project,
  29. onClickSetupLater,
  30. isLast,
  31. hasFirstEvent,
  32. handleFirstIssueReceived,
  33. }: FirstEventFooterProps) {
  34. const source = 'targeted_onboarding_first_event_footer';
  35. const [clientState, setClientState] = usePersistedOnboardingState();
  36. const getSecondaryCta = () => {
  37. // if hasn't sent first event, allow skiping.
  38. // if last, no secondary cta
  39. if (!hasFirstEvent && !isLast) {
  40. return <Button onClick={onClickSetupLater}>{t('Next Platform')}</Button>;
  41. }
  42. return null;
  43. };
  44. const getPrimaryCta = ({firstIssue}: {firstIssue: null | boolean | Group}) => {
  45. // if hasn't sent first event, allow creation of sample error
  46. if (!hasFirstEvent) {
  47. return (
  48. <CreateSampleEventButton
  49. project={project}
  50. source="targted-onboarding"
  51. priority="primary"
  52. >
  53. {t('View Sample Error')}
  54. </CreateSampleEventButton>
  55. );
  56. }
  57. return (
  58. <Button
  59. to={`/organizations/${organization.slug}/issues/${
  60. firstIssue && firstIssue !== true && 'id' in firstIssue
  61. ? `${firstIssue.id}/`
  62. : ''
  63. }?referrer=onboarding-first-event-footer`}
  64. priority="primary"
  65. >
  66. {t('Take me to my error')}
  67. </Button>
  68. );
  69. };
  70. return (
  71. <GridFooter>
  72. <SkipOnboardingLink
  73. onClick={() => {
  74. trackAdvancedAnalyticsEvent('growth.onboarding_clicked_skip', {
  75. organization,
  76. source,
  77. });
  78. if (clientState) {
  79. setClientState({
  80. ...clientState,
  81. state: 'skipped',
  82. });
  83. }
  84. }}
  85. to={`/organizations/${organization.slug}/issues/?referrer=onboarding-first-event-footer-skip`}
  86. >
  87. {t('Skip Onboarding')}
  88. </SkipOnboardingLink>
  89. <EventWaiter
  90. eventType="error"
  91. onIssueReceived={handleFirstIssueReceived}
  92. {...{project, organization}}
  93. >
  94. {({firstIssue}) => (
  95. <Fragment>
  96. <StatusWrapper>
  97. {hasFirstEvent ? (
  98. <IconCheckmark isCircled color="green400" />
  99. ) : (
  100. <WaitingIndicator />
  101. )}
  102. <AnimatedText errorReceived={hasFirstEvent}>
  103. {hasFirstEvent ? t('Error Received') : t('Waiting for error')}
  104. </AnimatedText>
  105. </StatusWrapper>
  106. <OnboardingButtonBar gap={2}>
  107. {getSecondaryCta()}
  108. {getPrimaryCta({firstIssue})}
  109. </OnboardingButtonBar>
  110. </Fragment>
  111. )}
  112. </EventWaiter>
  113. </GridFooter>
  114. );
  115. }
  116. const OnboardingButtonBar = styled(ButtonBar)`
  117. margin: ${space(2)} ${space(4)};
  118. justify-self: end;
  119. margin-left: auto;
  120. `;
  121. const AnimatedText = styled(motion.div, {
  122. shouldForwardProp: prop => prop !== 'errorReceived',
  123. })<{errorReceived: boolean}>`
  124. margin-left: ${space(1)};
  125. color: ${p => (p.errorReceived ? p.theme.successText : p.theme.pink400)};
  126. `;
  127. const indicatorAnimation: Variants = {
  128. initial: {opacity: 0, y: -10},
  129. animate: {opacity: 1, y: 0},
  130. exit: {opacity: 0, y: 10},
  131. };
  132. AnimatedText.defaultProps = {
  133. variants: indicatorAnimation,
  134. transition: testableTransition(),
  135. };
  136. const WaitingIndicator = styled(motion.div)`
  137. ${pulsingIndicatorStyles};
  138. background-color: ${p => p.theme.pink300};
  139. `;
  140. WaitingIndicator.defaultProps = {
  141. variants: indicatorAnimation,
  142. transition: testableTransition(),
  143. };
  144. const StatusWrapper = styled(motion.div)`
  145. display: flex;
  146. align-items: center;
  147. font-size: ${p => p.theme.fontSizeMedium};
  148. justify-content: center;
  149. @media (max-width: ${p => p.theme.breakpoints.small}) {
  150. display: none;
  151. }
  152. `;
  153. StatusWrapper.defaultProps = {
  154. initial: 'initial',
  155. animate: 'animate',
  156. exit: 'exit',
  157. variants: {
  158. initial: {opacity: 0, y: -10},
  159. animate: {
  160. opacity: 1,
  161. y: 0,
  162. transition: testableTransition({when: 'beforeChildren', staggerChildren: 0.35}),
  163. },
  164. exit: {opacity: 0, y: 10},
  165. },
  166. };
  167. const SkipOnboardingLink = styled(Link)`
  168. margin: auto ${space(4)};
  169. white-space: nowrap;
  170. @media (max-width: ${p => p.theme.breakpoints.small}) {
  171. display: none;
  172. }
  173. `;
  174. const GridFooter = styled(GenericFooter)`
  175. display: grid;
  176. grid-template-columns: 1fr 1fr 1fr;
  177. @media (max-width: ${p => p.theme.breakpoints.small}) {
  178. display: flex;
  179. flex-direction: row;
  180. justify-content: end;
  181. }
  182. `;