firstEventIndicator.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import styled from '@emotion/styled';
  2. import type {HTMLMotionProps, Variants} from 'framer-motion';
  3. import {AnimatePresence, motion} from 'framer-motion';
  4. import {Button} 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';
  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. <Button
  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. })
  37. }
  38. to={`/organizations/${props.organization.slug}/issues/${
  39. firstIssue && firstIssue !== true && 'id' in firstIssue
  40. ? `${firstIssue.id}/`
  41. : ''
  42. }?referrer=onboarding-first-event-indicator`}
  43. >
  44. {t('Take me to my error')}
  45. </Button>
  46. ),
  47. })
  48. }
  49. </EventWaiter>
  50. );
  51. }
  52. interface IndicatorProps extends Omit<EventWaiterProps, 'children' | 'api'> {
  53. firstIssue: null | boolean | Group;
  54. }
  55. function Indicator({firstIssue}: IndicatorProps) {
  56. return (
  57. <Container>
  58. <AnimatePresence>
  59. {!firstIssue ? <Waiting key="waiting" /> : <Success key="received" />}
  60. </AnimatePresence>
  61. </Container>
  62. );
  63. }
  64. const Container = styled('div')`
  65. display: grid;
  66. grid-template-columns: 1fr;
  67. justify-content: right;
  68. `;
  69. const StatusWrapper = styled(motion.div)`
  70. display: grid;
  71. grid-template-columns: 1fr max-content;
  72. gap: ${space(1)};
  73. align-items: center;
  74. font-size: ${p => p.theme.fontSizeMedium};
  75. /* Keep the wrapper in the parent grids first cell for transitions */
  76. grid-column: 1;
  77. grid-row: 1;
  78. `;
  79. StatusWrapper.defaultProps = {
  80. initial: 'initial',
  81. animate: 'animate',
  82. exit: 'exit',
  83. variants: {
  84. initial: {opacity: 0, y: -10},
  85. animate: {
  86. opacity: 1,
  87. y: 0,
  88. transition: testableTransition({when: 'beforeChildren', staggerChildren: 0.35}),
  89. },
  90. exit: {opacity: 0, y: 10},
  91. },
  92. };
  93. function Waiting(props: HTMLMotionProps<'div'>) {
  94. return (
  95. <StatusWrapper {...props}>
  96. <AnimatedText>{t('Waiting to receive first event to continue')}</AnimatedText>
  97. <WaitingIndicator />
  98. </StatusWrapper>
  99. );
  100. }
  101. function Success(props: HTMLMotionProps<'div'>) {
  102. return (
  103. <StatusWrapper {...props}>
  104. <AnimatedText>{t('Event was received!')}</AnimatedText>
  105. <ReceivedIndicator />
  106. </StatusWrapper>
  107. );
  108. }
  109. const indicatorAnimation: Variants = {
  110. initial: {opacity: 0, y: -10},
  111. animate: {opacity: 1, y: 0},
  112. exit: {opacity: 0, y: 10},
  113. };
  114. const AnimatedText = styled(motion.div)``;
  115. AnimatedText.defaultProps = {
  116. variants: indicatorAnimation,
  117. transition: testableTransition(),
  118. };
  119. const WaitingIndicator = styled(motion.div)`
  120. margin: 0 6px;
  121. ${pulsingIndicatorStyles};
  122. `;
  123. WaitingIndicator.defaultProps = {
  124. variants: indicatorAnimation,
  125. transition: testableTransition(),
  126. };
  127. const ReceivedIndicator = styled(IconCheckmark)`
  128. color: #fff;
  129. background: ${p => p.theme.green300};
  130. border-radius: 50%;
  131. padding: 3px;
  132. margin: 0 ${space(0.25)};
  133. `;
  134. ReceivedIndicator.defaultProps = {
  135. size: 'sm',
  136. };
  137. export {Indicator};
  138. export default FirstEventIndicator;