replayPreviewPlayer.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import type {ComponentProps} from 'react';
  2. import {useEffect, useRef, useState} from 'react';
  3. import styled from '@emotion/styled';
  4. import {Button, LinkButton} from 'sentry/components/button';
  5. import ErrorBoundary from 'sentry/components/errorBoundary';
  6. import Panel from 'sentry/components/panels/panel';
  7. import {useReplayContext} from 'sentry/components/replays/replayContext';
  8. import ReplayCurrentScreen from 'sentry/components/replays/replayCurrentScreen';
  9. import ReplayCurrentUrl from 'sentry/components/replays/replayCurrentUrl';
  10. import {ReplayFullscreenButton} from 'sentry/components/replays/replayFullscreenButton';
  11. import ReplayPlayer from 'sentry/components/replays/replayPlayer';
  12. import ReplayPlayPauseButton from 'sentry/components/replays/replayPlayPauseButton';
  13. import {ReplaySidebarToggleButton} from 'sentry/components/replays/replaySidebarToggleButton';
  14. import TimeAndScrubberGrid from 'sentry/components/replays/timeAndScrubberGrid';
  15. import {IconNext, IconPrevious} from 'sentry/icons';
  16. import {t} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import EventView from 'sentry/utils/discover/eventView';
  19. import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
  20. import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
  21. import useMarkReplayViewed from 'sentry/utils/replays/hooks/useMarkReplayViewed';
  22. import {useLocation} from 'sentry/utils/useLocation';
  23. import useOrganization from 'sentry/utils/useOrganization';
  24. import {useRoutes} from 'sentry/utils/useRoutes';
  25. import useFullscreen from 'sentry/utils/window/useFullscreen';
  26. import useIsFullscreen from 'sentry/utils/window/useIsFullscreen';
  27. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  28. import Breadcrumbs from 'sentry/views/replays/detail/breadcrumbs';
  29. import BrowserOSIcons from 'sentry/views/replays/detail/browserOSIcons';
  30. import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
  31. import {ReplayCell} from 'sentry/views/replays/replayTable/tableCell';
  32. import type {ReplayRecord} from 'sentry/views/replays/types';
  33. function ReplayPreviewPlayer({
  34. replayId,
  35. fullReplayButtonProps,
  36. replayRecord,
  37. handleBackClick,
  38. handleForwardClick,
  39. overlayContent,
  40. showNextAndPrevious,
  41. playPausePriority,
  42. }: {
  43. replayId: string;
  44. replayRecord: ReplayRecord;
  45. fullReplayButtonProps?: Partial<ComponentProps<typeof LinkButton>>;
  46. handleBackClick?: () => void;
  47. handleForwardClick?: () => void;
  48. overlayContent?: React.ReactNode;
  49. playPausePriority?: ComponentProps<typeof ReplayPlayPauseButton>['priority'];
  50. showNextAndPrevious?: boolean;
  51. }) {
  52. const routes = useRoutes();
  53. const location = useLocation();
  54. const organization = useOrganization();
  55. const [isSidebarOpen, setIsSidebarOpen] = useState(true);
  56. const {replay, currentTime, isFetching, isFinished, isPlaying, isVideoReplay} =
  57. useReplayContext();
  58. const eventView = EventView.fromLocation(location);
  59. const fullscreenRef = useRef(null);
  60. const {toggle: toggleFullscreen} = useFullscreen({
  61. elementRef: fullscreenRef,
  62. });
  63. const isFullscreen = useIsFullscreen();
  64. const startOffsetMs = replay?.getStartOffsetMs() ?? 0;
  65. const referrer = getRouteStringFromRoutes(routes);
  66. const fromFeedback = referrer === '/feedback/';
  67. const fullReplayUrl = {
  68. pathname: normalizeUrl(`/organizations/${organization.slug}/replays/${replayId}/`),
  69. query: {
  70. referrer: getRouteStringFromRoutes(routes),
  71. t_main: fromFeedback ? TabKey.BREADCRUMBS : TabKey.ERRORS,
  72. t: (currentTime + startOffsetMs) / 1000,
  73. f_b_type: fromFeedback ? 'feedback' : undefined,
  74. },
  75. };
  76. const {mutate: markAsViewed} = useMarkReplayViewed();
  77. useEffect(() => {
  78. if (replayRecord?.id && !replayRecord.has_viewed && !isFetching && isPlaying) {
  79. markAsViewed({projectSlug: replayRecord.project_id, replayId: replayRecord.id});
  80. }
  81. }, [isFetching, isPlaying, markAsViewed, organization, replayRecord]);
  82. return (
  83. <PlayerPanel>
  84. <HeaderWrapper>
  85. <StyledReplayCell
  86. key="session"
  87. replay={replayRecord}
  88. eventView={eventView}
  89. organization={organization}
  90. referrer="issue-details-replay-header"
  91. />
  92. <LinkButton size="sm" to={fullReplayUrl} {...fullReplayButtonProps}>
  93. {t('See Full Replay')}
  94. </LinkButton>
  95. </HeaderWrapper>
  96. <PreviewPlayerContainer ref={fullscreenRef} isSidebarOpen={isSidebarOpen}>
  97. <PlayerBreadcrumbContainer>
  98. <PlayerContextContainer>
  99. {isFullscreen ? (
  100. <ContextContainer>
  101. {isVideoReplay ? <ReplayCurrentScreen /> : <ReplayCurrentUrl />}
  102. <BrowserOSIcons />
  103. <ReplaySidebarToggleButton
  104. isOpen={isSidebarOpen}
  105. setIsOpen={setIsSidebarOpen}
  106. />
  107. </ContextContainer>
  108. ) : null}
  109. <StaticPanel>
  110. <ReplayPlayer overlayContent={overlayContent} />
  111. </StaticPanel>
  112. </PlayerContextContainer>
  113. {isFullscreen && isSidebarOpen ? <Breadcrumbs /> : null}
  114. </PlayerBreadcrumbContainer>
  115. <ErrorBoundary mini>
  116. <ButtonGrid>
  117. {showNextAndPrevious && (
  118. <Button
  119. size="sm"
  120. title={t('Previous Clip')}
  121. icon={<IconPrevious />}
  122. onClick={() => handleBackClick?.()}
  123. aria-label={t('Previous Clip')}
  124. disabled={!handleBackClick}
  125. analyticsEventName="Replay Preview Player: Clicked Previous Clip"
  126. analyticsEventKey="replay_preview_player.clicked_previous_clip"
  127. />
  128. )}
  129. <ReplayPlayPauseButton
  130. analyticsEventName="Replay Preview Player: Clicked Play/Plause Clip"
  131. analyticsEventKey="replay_preview_player.clicked_play_pause_clip"
  132. priority={
  133. playPausePriority ?? (isFinished || isPlaying ? 'primary' : 'default')
  134. }
  135. />
  136. {showNextAndPrevious && (
  137. <Button
  138. size="sm"
  139. title={t('Next Clip')}
  140. icon={<IconNext />}
  141. onClick={() => handleForwardClick?.()}
  142. aria-label={t('Next Clip')}
  143. disabled={!handleForwardClick}
  144. analyticsEventName="Replay Preview Player: Clicked Next Clip"
  145. analyticsEventKey="replay_preview_player.clicked_next_clip"
  146. />
  147. )}
  148. <Container>
  149. <TimeAndScrubberGrid />
  150. </Container>
  151. <ReplayFullscreenButton toggleFullscreen={toggleFullscreen} />
  152. </ButtonGrid>
  153. </ErrorBoundary>
  154. </PreviewPlayerContainer>
  155. </PlayerPanel>
  156. );
  157. }
  158. const PlayerPanel = styled(Panel)`
  159. padding: ${space(3)} ${space(3)} ${space(1.5)};
  160. margin: 0;
  161. display: flex;
  162. gap: ${space(1)};
  163. flex-direction: column;
  164. flex-grow: 1;
  165. overflow: hidden;
  166. height: 100%;
  167. `;
  168. const PlayerBreadcrumbContainer = styled(FluidHeight)`
  169. position: relative;
  170. `;
  171. const PreviewPlayerContainer = styled(FluidHeight)<{isSidebarOpen: boolean}>`
  172. gap: ${space(2)};
  173. background: ${p => p.theme.background};
  174. :fullscreen {
  175. padding: ${space(1)};
  176. ${PlayerBreadcrumbContainer} {
  177. display: grid;
  178. grid-template-columns: ${p => (p.isSidebarOpen ? '1fr 25%' : '1fr')};
  179. height: 100%;
  180. gap: ${space(1)};
  181. }
  182. }
  183. `;
  184. const PlayerContextContainer = styled(FluidHeight)`
  185. display: flex;
  186. flex-direction: column;
  187. gap: ${space(1)};
  188. `;
  189. const StaticPanel = styled(FluidHeight)`
  190. border: 1px solid ${p => p.theme.border};
  191. border-radius: ${p => p.theme.borderRadius};
  192. `;
  193. const ButtonGrid = styled('div')`
  194. display: flex;
  195. align-items: center;
  196. gap: 0 ${space(1)};
  197. flex-direction: row;
  198. justify-content: space-between;
  199. `;
  200. const Container = styled('div')`
  201. display: flex;
  202. flex-direction: column;
  203. flex: 1 1;
  204. justify-content: center;
  205. `;
  206. const ContextContainer = styled('div')`
  207. display: grid;
  208. grid-auto-flow: column;
  209. grid-template-columns: 1fr max-content max-content;
  210. align-items: center;
  211. gap: ${space(1)};
  212. `;
  213. const StyledReplayCell = styled(ReplayCell)`
  214. padding: 0 0 ${space(1)};
  215. `;
  216. const HeaderWrapper = styled('div')`
  217. display: flex;
  218. justify-content: space-between;
  219. align-items: center;
  220. margin-bottom: ${space(1)};
  221. `;
  222. export default ReplayPreviewPlayer;