firstEventFooter.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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 | true | 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 !== true && firstIssue !== null ? `${firstIssue.id}/` : ''
  61. }`}
  62. priority="primary"
  63. >
  64. {t('Take me to my error')}
  65. </Button>
  66. );
  67. };
  68. return (
  69. <GridFooter>
  70. <SkipOnboardingLink
  71. onClick={() => {
  72. trackAdvancedAnalyticsEvent('growth.onboarding_clicked_skip', {
  73. organization,
  74. source,
  75. });
  76. if (clientState) {
  77. setClientState({
  78. ...clientState,
  79. state: 'skipped',
  80. });
  81. }
  82. }}
  83. to={`/organizations/${organization.slug}/issues/`}
  84. >
  85. {t('Skip Onboarding')}
  86. </SkipOnboardingLink>
  87. <EventWaiter
  88. eventType="error"
  89. onIssueReceived={handleFirstIssueReceived}
  90. {...{project, organization}}
  91. >
  92. {({firstIssue}) => (
  93. <Fragment>
  94. <StatusWrapper>
  95. {hasFirstEvent ? (
  96. <IconCheckmark isCircled color="green400" />
  97. ) : (
  98. <WaitingIndicator />
  99. )}
  100. <AnimatedText errorReceived={hasFirstEvent}>
  101. {hasFirstEvent ? t('Error Received') : t('Waiting for error')}
  102. </AnimatedText>
  103. </StatusWrapper>
  104. <OnboardingButtonBar gap={2}>
  105. {getSecondaryCta()}
  106. {getPrimaryCta({firstIssue})}
  107. </OnboardingButtonBar>
  108. </Fragment>
  109. )}
  110. </EventWaiter>
  111. </GridFooter>
  112. );
  113. }
  114. const OnboardingButtonBar = styled(ButtonBar)`
  115. margin: ${space(2)} ${space(4)};
  116. justify-self: end;
  117. margin-left: auto;
  118. `;
  119. const AnimatedText = styled(motion.div, {
  120. shouldForwardProp: prop => prop !== 'errorReceived',
  121. })<{errorReceived: boolean}>`
  122. margin-left: ${space(1)};
  123. color: ${p => (p.errorReceived ? p.theme.successText : p.theme.pink300)};
  124. `;
  125. const indicatorAnimation: Variants = {
  126. initial: {opacity: 0, y: -10},
  127. animate: {opacity: 1, y: 0},
  128. exit: {opacity: 0, y: 10},
  129. };
  130. AnimatedText.defaultProps = {
  131. variants: indicatorAnimation,
  132. transition: testableTransition(),
  133. };
  134. const WaitingIndicator = styled(motion.div)`
  135. ${pulsingIndicatorStyles};
  136. background-color: ${p => p.theme.pink300};
  137. `;
  138. WaitingIndicator.defaultProps = {
  139. variants: indicatorAnimation,
  140. transition: testableTransition(),
  141. };
  142. const StatusWrapper = styled(motion.div)`
  143. display: flex;
  144. align-items: center;
  145. font-size: ${p => p.theme.fontSizeMedium};
  146. justify-content: center;
  147. @media (max-width: ${p => p.theme.breakpoints.small}) {
  148. display: none;
  149. }
  150. `;
  151. StatusWrapper.defaultProps = {
  152. initial: 'initial',
  153. animate: 'animate',
  154. exit: 'exit',
  155. variants: {
  156. initial: {opacity: 0, y: -10},
  157. animate: {
  158. opacity: 1,
  159. y: 0,
  160. transition: testableTransition({when: 'beforeChildren', staggerChildren: 0.35}),
  161. },
  162. exit: {opacity: 0, y: 10},
  163. },
  164. };
  165. const SkipOnboardingLink = styled(Link)`
  166. margin: auto ${space(4)};
  167. white-space: nowrap;
  168. @media (max-width: ${p => p.theme.breakpoints.small}) {
  169. display: none;
  170. }
  171. `;
  172. const GridFooter = styled(GenericFooter)`
  173. display: grid;
  174. grid-template-columns: 1fr 1fr 1fr;
  175. @media (max-width: ${p => p.theme.breakpoints.small}) {
  176. display: flex;
  177. flex-direction: row;
  178. justify-content: end;
  179. }
  180. `;