autofixOutputStream.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import {useEffect, useRef, useState} from 'react';
  2. import {keyframes} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {AnimatePresence, motion} from 'framer-motion';
  5. import {IconArrow} from 'sentry/icons';
  6. import {space} from 'sentry/styles/space';
  7. import testableTransition from 'sentry/utils/testableTransition';
  8. interface Props {
  9. stream: string;
  10. }
  11. const shimmer = keyframes`
  12. 0% {
  13. background-position: -1000px 0;
  14. }
  15. 100% {
  16. background-position: 1000px 0;
  17. }
  18. `;
  19. export function AutofixOutputStream({stream}: Props) {
  20. const [displayedText, setDisplayedText] = useState('');
  21. const previousText = useRef('');
  22. const currentIndexRef = useRef(0);
  23. useEffect(() => {
  24. const newText = stream;
  25. // Reset animation if the new text is completely different
  26. if (!newText.startsWith(displayedText)) {
  27. previousText.current = newText;
  28. currentIndexRef.current = 0;
  29. setDisplayedText('');
  30. }
  31. const interval = window.setInterval(() => {
  32. if (currentIndexRef.current < newText.length) {
  33. setDisplayedText(newText.slice(0, currentIndexRef.current + 1));
  34. currentIndexRef.current++;
  35. } else {
  36. window.clearInterval(interval);
  37. }
  38. }, 15);
  39. return () => {
  40. window.clearInterval(interval);
  41. };
  42. }, [displayedText, stream]);
  43. return (
  44. <AnimatePresence mode="wait">
  45. <Wrapper
  46. key="output-stream"
  47. initial={{opacity: 0, height: 0, scale: 0.8}}
  48. animate={{opacity: 1, height: 'auto', scale: 1}}
  49. exit={{opacity: 0, height: 0, scale: 0.8, y: -20}}
  50. transition={testableTransition({
  51. duration: 1.0,
  52. height: {
  53. type: 'spring',
  54. bounce: 0.2,
  55. },
  56. scale: {
  57. type: 'spring',
  58. bounce: 0.2,
  59. },
  60. y: {
  61. type: 'tween',
  62. ease: 'easeOut',
  63. },
  64. })}
  65. style={{
  66. transformOrigin: 'top center',
  67. }}
  68. >
  69. <StyledArrow direction="down" size="sm" />
  70. <StreamContainer layout>
  71. <StreamContent>{displayedText}</StreamContent>
  72. </StreamContainer>
  73. </Wrapper>
  74. </AnimatePresence>
  75. );
  76. }
  77. const Wrapper = styled(motion.div)`
  78. display: flex;
  79. flex-direction: column;
  80. align-items: center;
  81. margin: ${space(1)} ${space(4)};
  82. gap: ${space(1)};
  83. overflow: hidden;
  84. `;
  85. const StreamContainer = styled(motion.div)`
  86. position: relative;
  87. width: 100%;
  88. border-radius: ${p => p.theme.borderRadius};
  89. background: ${p => p.theme.background};
  90. border: 1px dashed ${p => p.theme.border};
  91. height: 5rem;
  92. overflow: hidden;
  93. &:before {
  94. content: '';
  95. position: absolute;
  96. inset: 0;
  97. background: linear-gradient(
  98. 90deg,
  99. transparent,
  100. ${p => p.theme.active}20,
  101. transparent
  102. );
  103. background-size: 2000px 100%;
  104. animation: ${shimmer} 2s infinite linear;
  105. pointer-events: none;
  106. }
  107. `;
  108. const StreamContent = styled('div')`
  109. margin: 0;
  110. padding: ${space(2)};
  111. white-space: pre-wrap;
  112. word-break: break-word;
  113. font-size: ${p => p.theme.fontSizeSmall};
  114. color: ${p => p.theme.subText};
  115. height: 5rem;
  116. overflow-y: auto;
  117. display: flex;
  118. flex-direction: column-reverse;
  119. `;
  120. const StyledArrow = styled(IconArrow)`
  121. color: ${p => p.theme.subText};
  122. opacity: 0.5;
  123. `;