replayPreviewPlayer.tsx 7.2 KB

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