firstEventIndicator.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import styled from '@emotion/styled';
  2. import type {HTMLMotionProps, Variants} from 'framer-motion';
  3. import {AnimatePresence, motion} from 'framer-motion';
  4. import {LinkButton} from 'sentry/components/button';
  5. import {IconCheckmark} from 'sentry/icons';
  6. import {t} from 'sentry/locale';
  7. import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
  8. import {space} from 'sentry/styles/space';
  9. import type {Group} from 'sentry/types/group';
  10. import {trackAnalytics} from 'sentry/utils/analytics';
  11. import type {EventWaiterProps} from 'sentry/utils/eventWaiter';
  12. import EventWaiter from 'sentry/utils/eventWaiter';
  13. import testableTransition from 'sentry/utils/testableTransition';
  14. type RenderProps = {
  15. firstEventButton: React.ReactNode;
  16. indicator: React.ReactNode;
  17. };
  18. interface FirstEventIndicatorProps extends Omit<EventWaiterProps, 'children' | 'api'> {
  19. children: (props: RenderProps) => React.ReactNode;
  20. }
  21. function FirstEventIndicator({children, ...props}: FirstEventIndicatorProps) {
  22. return (
  23. <EventWaiter {...props}>
  24. {({firstIssue}) =>
  25. children({
  26. indicator: <Indicator firstIssue={firstIssue} {...props} />,
  27. firstEventButton: (
  28. <LinkButton
  29. title={t("You'll need to send your first error to continue")}
  30. tooltipProps={{disabled: !!firstIssue}}
  31. disabled={!firstIssue}
  32. priority="primary"
  33. onClick={() =>
  34. trackAnalytics('growth.onboarding_take_to_error', {
  35. organization: props.organization,
  36. platform: props.project.platform,
  37. })
  38. }
  39. to={`/organizations/${props.organization.slug}/issues/${
  40. firstIssue && firstIssue !== true && 'id' in firstIssue
  41. ? `${firstIssue.id}/`
  42. : ''
  43. }?referrer=onboarding-first-event-indicator`}
  44. >
  45. {t('Take me to my error')}
  46. </LinkButton>
  47. ),
  48. })
  49. }
  50. </EventWaiter>
  51. );
  52. }
  53. interface IndicatorProps extends Omit<EventWaiterProps, 'children' | 'api'> {
  54. firstIssue: null | boolean | Group;
  55. }
  56. function Indicator({firstIssue}: IndicatorProps) {
  57. return (
  58. <Container>
  59. <AnimatePresence>
  60. {!firstIssue ? <Waiting key="waiting" /> : <Success key="received" />}
  61. </AnimatePresence>
  62. </Container>
  63. );
  64. }
  65. const Container = styled('div')`
  66. display: grid;
  67. grid-template-columns: 1fr;
  68. justify-content: right;
  69. `;
  70. const StatusWrapper = styled(motion.div)`
  71. display: grid;
  72. grid-template-columns: 1fr max-content;
  73. gap: ${space(1)};
  74. align-items: center;
  75. font-size: ${p => p.theme.fontSizeMedium};
  76. /* Keep the wrapper in the parent grids first cell for transitions */
  77. grid-column: 1;
  78. grid-row: 1;
  79. `;
  80. StatusWrapper.defaultProps = {
  81. initial: 'initial',
  82. animate: 'animate',
  83. exit: 'exit',
  84. variants: {
  85. initial: {opacity: 0, y: -10},
  86. animate: {
  87. opacity: 1,
  88. y: 0,
  89. transition: testableTransition({when: 'beforeChildren', staggerChildren: 0.35}),
  90. },
  91. exit: {opacity: 0, y: 10},
  92. },
  93. };
  94. function Waiting(props: HTMLMotionProps<'div'>) {
  95. return (
  96. <StatusWrapper {...props}>
  97. <AnimatedText>{t('Waiting to receive first event to continue')}</AnimatedText>
  98. <WaitingIndicator />
  99. </StatusWrapper>
  100. );
  101. }
  102. function Success(props: HTMLMotionProps<'div'>) {
  103. return (
  104. <StatusWrapper {...props}>
  105. <AnimatedText>{t('Event was received!')}</AnimatedText>
  106. <ReceivedIndicator />
  107. </StatusWrapper>
  108. );
  109. }
  110. const indicatorAnimation: Variants = {
  111. initial: {opacity: 0, y: -10},
  112. animate: {opacity: 1, y: 0},
  113. exit: {opacity: 0, y: 10},
  114. };
  115. const AnimatedText = styled(motion.div)``;
  116. AnimatedText.defaultProps = {
  117. variants: indicatorAnimation,
  118. transition: testableTransition(),
  119. };
  120. const WaitingIndicator = styled(motion.div)`
  121. margin: 0 6px;
  122. ${pulsingIndicatorStyles};
  123. `;
  124. WaitingIndicator.defaultProps = {
  125. variants: indicatorAnimation,
  126. transition: testableTransition(),
  127. };
  128. const ReceivedIndicator = styled(IconCheckmark)`
  129. color: #fff;
  130. background: ${p => p.theme.green300};
  131. border-radius: 50%;
  132. padding: 3px;
  133. margin: 0 ${space(0.25)};
  134. `;
  135. ReceivedIndicator.defaultProps = {
  136. size: 'sm',
  137. };
  138. export {Indicator};
  139. export default FirstEventIndicator;