replayController.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import Button from 'sentry/components/button';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import {transformCrumbs} from 'sentry/components/events/interfaces/breadcrumbs/utils';
  6. import CompactSelect from 'sentry/components/forms/compactSelect';
  7. import {useReplayContext} from 'sentry/components/replays/replayContext';
  8. import {formatTime, relativeTimeInMs} from 'sentry/components/replays/utils';
  9. import {
  10. IconArrow,
  11. IconNext,
  12. IconPause,
  13. IconPlay,
  14. IconPrevious,
  15. IconRefresh,
  16. IconResize,
  17. } from 'sentry/icons';
  18. import {t} from 'sentry/locale';
  19. import space from 'sentry/styles/space';
  20. import {BreadcrumbType} from 'sentry/types/breadcrumbs';
  21. import {getNextBreadcrumb} from 'sentry/utils/replays/getBreadcrumb';
  22. import useFullscreen from 'sentry/utils/replays/hooks/useFullscreen';
  23. const SECOND = 1000;
  24. const USER_ACTIONS = [
  25. BreadcrumbType.ERROR,
  26. BreadcrumbType.INIT,
  27. BreadcrumbType.NAVIGATION,
  28. BreadcrumbType.UI,
  29. BreadcrumbType.USER,
  30. ];
  31. interface Props {
  32. speedOptions?: number[];
  33. toggleFullscreen?: () => void;
  34. }
  35. function ReplayPlayPauseBar() {
  36. const {
  37. currentTime,
  38. isFinished,
  39. isPlaying,
  40. replay,
  41. restart,
  42. setCurrentTime,
  43. togglePlayPause,
  44. } = useReplayContext();
  45. return (
  46. <ButtonBar merged>
  47. <Button
  48. size="xsmall"
  49. title={t('Go back 10 seconds')}
  50. icon={<IconRefresh size="sm" />}
  51. onClick={() => setCurrentTime(currentTime - 10 * SECOND)}
  52. aria-label={t('Go back 10 seconds')}
  53. />
  54. {isFinished ? (
  55. <Button
  56. size="xsmall"
  57. title={t('Restart Replay')}
  58. icon={<IconPrevious size="sm" />}
  59. onClick={restart}
  60. aria-label={t('Restart the Replay')}
  61. />
  62. ) : (
  63. <Button
  64. size="xsmall"
  65. title={isPlaying ? t('Pause the Replay') : t('Play the Replay')}
  66. icon={isPlaying ? <IconPause size="sm" /> : <IconPlay size="sm" />}
  67. onClick={() => togglePlayPause(!isPlaying)}
  68. aria-label={isPlaying ? t('Pause the Replay') : t('Play the Replay')}
  69. />
  70. )}
  71. <Button
  72. size="xsmall"
  73. title={t('Jump to next event')}
  74. icon={<IconNext size="sm" />}
  75. onClick={() => {
  76. const startTimestampSec = replay?.getEvent().startTimestamp;
  77. if (!startTimestampSec) {
  78. return;
  79. }
  80. const transformedCrumbs = transformCrumbs(replay?.getRawCrumbs() || []);
  81. const next = getNextBreadcrumb({
  82. crumbs: transformedCrumbs.filter(crumb => USER_ACTIONS.includes(crumb.type)),
  83. targetTimestampMs: startTimestampSec * 1000 + currentTime,
  84. });
  85. if (startTimestampSec !== undefined && next?.timestamp) {
  86. setCurrentTime(relativeTimeInMs(next.timestamp, startTimestampSec));
  87. }
  88. }}
  89. aria-label={t('Jump to next event')}
  90. />
  91. </ButtonBar>
  92. );
  93. }
  94. function ReplayCurrentTime() {
  95. const {currentTime, duration} = useReplayContext();
  96. return (
  97. <span>
  98. {formatTime(currentTime)} / {duration ? formatTime(duration) : '??:??'}
  99. </span>
  100. );
  101. }
  102. function ReplayPlaybackSpeed({speedOptions}: {speedOptions: number[]}) {
  103. const {setSpeed, speed} = useReplayContext();
  104. return (
  105. <CompactSelect
  106. triggerProps={{
  107. size: 'xsmall',
  108. prefix: t('Speed'),
  109. }}
  110. value={speed}
  111. options={speedOptions.map(speedOption => ({
  112. value: speedOption,
  113. label: `${speedOption}x`,
  114. disabled: speedOption === speed,
  115. }))}
  116. onChange={opt => {
  117. setSpeed(opt.value);
  118. }}
  119. />
  120. );
  121. }
  122. const ReplayControls = ({
  123. toggleFullscreen = () => {},
  124. speedOptions = [0.1, 0.25, 0.5, 1, 2, 4],
  125. }: Props) => {
  126. const {isFullscreen} = useFullscreen();
  127. const {isSkippingInactive, toggleSkipInactive} = useReplayContext();
  128. return (
  129. <ButtonGrid>
  130. <ReplayPlayPauseBar />
  131. <ReplayCurrentTime />
  132. {/* TODO(replay): Need a better icon for the FastForward toggle */}
  133. <Button
  134. size="xsmall"
  135. title={t('Fast-forward idle moments')}
  136. aria-label={t('Fast-forward idle moments')}
  137. icon={<IconArrow size="sm" direction="right" />}
  138. priority={isSkippingInactive ? 'primary' : undefined}
  139. onClick={() => toggleSkipInactive(!isSkippingInactive)}
  140. />
  141. <ReplayPlaybackSpeed speedOptions={speedOptions} />
  142. <Button
  143. size="xsmall"
  144. title={isFullscreen ? t('Exit full screen') : t('View in full screen')}
  145. aria-label={isFullscreen ? t('Exit full screen') : t('View in full screen')}
  146. icon={<IconResize size="sm" />}
  147. priority={isFullscreen ? 'primary' : undefined}
  148. onClick={toggleFullscreen}
  149. />
  150. </ButtonGrid>
  151. );
  152. };
  153. const ButtonGrid = styled('div')`
  154. display: grid;
  155. grid-column-gap: ${space(1)};
  156. grid-template-columns: max-content auto max-content max-content max-content;
  157. align-items: center;
  158. `;
  159. export default ReplayControls;