baseRRWebReplayer.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import {useEffect, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import RRWebPlayer from 'rrweb-player';
  4. import space from 'sentry/styles/space';
  5. type RRWebEvents = ConstructorParameters<typeof RRWebPlayer>[0]['props']['events'];
  6. interface Props {
  7. className?: string;
  8. events?: RRWebEvents;
  9. }
  10. const BaseRRWebReplayerComponent = ({events, className}: Props) => {
  11. const playerEl = useRef<HTMLDivElement>(null);
  12. const initPlayer = () => {
  13. if (events === undefined) {
  14. return;
  15. }
  16. if (playerEl.current === null) {
  17. return;
  18. }
  19. // eslint-disable-next-line no-new
  20. new RRWebPlayer({
  21. target: playerEl.current,
  22. props: {events, autoPlay: false},
  23. });
  24. };
  25. useEffect(() => void initPlayer(), [events]);
  26. return <div ref={playerEl} className={className} />;
  27. };
  28. const BaseRRWebReplayer = styled(BaseRRWebReplayerComponent)`
  29. .replayer-mouse {
  30. position: absolute;
  31. width: 32px;
  32. height: 32px;
  33. transition: left 0.05s linear, top 0.05s linear;
  34. background-size: contain;
  35. background-repeat: no-repeat;
  36. background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTkiIHZpZXdCb3g9IjAgMCAxMiAxOSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgMTZWMEwxMS42IDExLjZINC44TDQuNCAxMS43TDAgMTZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOS4xIDE2LjdMNS41IDE4LjJMMC43OTk5OTkgNy4xTDQuNSA1LjZMOS4xIDE2LjdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNC42NzQ1MSA4LjYxODUxTDIuODMwMzEgOS4zOTI3MUw1LjkyNzExIDE2Ljc2OTVMNy43NzEzMSAxNS45OTUzTDQuNjc0NTEgOC42MTg1MVoiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik0xIDIuNFYxMy42TDQgMTAuN0w0LjQgMTAuNkg5LjJMMSAyLjRaIiBmaWxsPSJibGFjayIvPgo8L3N2Zz4K');
  37. border-color: transparent;
  38. }
  39. .replayer-mouse:after {
  40. content: '';
  41. display: inline-block;
  42. width: 32px;
  43. height: 32px;
  44. background: ${p => p.theme.purple300};
  45. border-radius: 100%;
  46. transform: translate(-50%, -50%);
  47. opacity: 0.3;
  48. }
  49. .replayer-mouse.active:after {
  50. animation: click 0.2s ease-in-out 1;
  51. }
  52. .replayer-mouse.touch-device {
  53. background-image: none;
  54. width: 70px;
  55. height: 70px;
  56. border-radius: 100%;
  57. margin-left: -37px;
  58. margin-top: -37px;
  59. border: 4px solid rgba(73, 80, 246, 0);
  60. transition: left 0s linear, top 0s linear, border-color 0.2s ease-in-out;
  61. }
  62. .replayer-mouse.touch-device.touch-active {
  63. border-color: ${p => p.theme.purple200};
  64. transition: left 0.25s linear, top 0.25s linear, border-color 0.2s ease-in-out;
  65. }
  66. .replayer-mouse.touch-device:after {
  67. opacity: 0;
  68. }
  69. .replayer-mouse.touch-device.active:after {
  70. animation: touch-click 0.2s ease-in-out 1;
  71. }
  72. .replayer-mouse-tail {
  73. position: absolute;
  74. pointer-events: none;
  75. }
  76. @keyframes click {
  77. 0% {
  78. opacity: 0.3;
  79. width: 20px;
  80. height: 20px;
  81. }
  82. 50% {
  83. opacity: 0.5;
  84. width: 10px;
  85. height: 10px;
  86. }
  87. }
  88. @keyframes touch-click {
  89. 0% {
  90. opacity: 0;
  91. width: 20px;
  92. height: 20px;
  93. }
  94. 50% {
  95. opacity: 0.5;
  96. width: 10px;
  97. height: 10px;
  98. }
  99. }
  100. .replayer-wrapper > iframe {
  101. border: none;
  102. }
  103. .rr-player {
  104. width: auto !important;
  105. height: auto !important;
  106. }
  107. .rr-player__frame {
  108. width: 100% !important;
  109. border-radius: 3px 3px 0 0;
  110. border: 1px solid ${p => p.theme.border};
  111. overflow: hidden;
  112. }
  113. .rr-player iframe {
  114. width: 100% !important;
  115. height: 100% !important;
  116. position: absolute;
  117. top: 0;
  118. right: 0;
  119. bottom: 0;
  120. left: 0;
  121. border: 0;
  122. }
  123. .replayer-wrapper {
  124. transform: scale(0.6) translate(0, 0) !important;
  125. transform-origin: top left;
  126. width: 166.66%;
  127. height: 166.66%;
  128. overflow: hidden;
  129. position: relative;
  130. box-shadow: inset 0 -1px 3px rgba(0, 0, 0, 0.08);
  131. }
  132. .replayer-mouse,
  133. .replayer-mouse:after {
  134. z-index: ${p => p.theme.zIndex.tooltip};
  135. }
  136. .rr-controller {
  137. width: 100%;
  138. display: block;
  139. padding: ${space(2)} 0;
  140. background: ${p => p.theme.background};
  141. border-radius: 0 0 3px 3px;
  142. border: 1px solid ${p => p.theme.border};
  143. border-top: none;
  144. position: relative;
  145. color: ${p => p.theme.textColor};
  146. }
  147. .rr-timeline {
  148. width: 100%;
  149. display: grid;
  150. grid-template-columns: 90px 1fr 90px;
  151. align-items: center;
  152. }
  153. .rr-timeline__time {
  154. text-align: center;
  155. color: ${p => p.theme.textColor};
  156. }
  157. .rr-progress {
  158. width: 100%;
  159. height: 12px;
  160. position: relative;
  161. cursor: pointer;
  162. &:before {
  163. content: '';
  164. background: ${p => p.theme.innerBorder};
  165. border-radius: 3px;
  166. display: block;
  167. height: 6px;
  168. position: absolute;
  169. left: 0;
  170. right: 0;
  171. top: 3px;
  172. }
  173. }
  174. .rr-progress.disabled {
  175. cursor: not-allowed;
  176. }
  177. .rr-progress__step {
  178. height: 6px;
  179. position: absolute;
  180. left: 0;
  181. top: 3px;
  182. border-radius: 3px;
  183. background: ${p => p.theme.purple200};
  184. }
  185. .rr-progress__handler {
  186. width: 20px;
  187. height: 20px;
  188. border-radius: 50%;
  189. position: absolute;
  190. top: 6px;
  191. transform: translate(-50%, -50%);
  192. background: ${p => p.theme.purple300};
  193. }
  194. .rr-controller__btns {
  195. display: grid;
  196. grid-auto-flow: column;
  197. grid-auto-columns: max-content;
  198. gap: ${space(0.75)};
  199. align-items: center;
  200. justify-content: center;
  201. font-size: ${p => p.theme.fontSizeSmall};
  202. }
  203. .rr-controller__btns button {
  204. color: ${p => p.theme.textColor};
  205. width: 28px;
  206. height: 28px;
  207. display: flex;
  208. padding: 0;
  209. align-items: center;
  210. justify-content: center;
  211. background: none;
  212. border: none;
  213. border-radius: 50%;
  214. cursor: pointer;
  215. transition: background 200ms ease;
  216. > svg {
  217. fill: ${p => p.theme.textColor};
  218. }
  219. }
  220. .rr-controller__btns button:active {
  221. background: ${p => p.theme.innerBorder};
  222. }
  223. .rr-controller__btns button.active {
  224. color: ${p => p.theme.white};
  225. background: ${p => p.theme.active};
  226. }
  227. .rr-controller__btns button:disabled {
  228. cursor: not-allowed;
  229. }
  230. .switch {
  231. height: 1em;
  232. display: flex;
  233. align-items: center;
  234. }
  235. .switch.disabled {
  236. opacity: 0.5;
  237. }
  238. .label {
  239. margin: 0 8px;
  240. }
  241. .switch input[type='checkbox'] {
  242. position: absolute;
  243. visibility: hidden;
  244. }
  245. .switch label {
  246. width: 32px;
  247. height: 16px;
  248. position: relative;
  249. cursor: pointer;
  250. display: flex;
  251. align-items: center;
  252. margin: 0;
  253. padding: 2px 3px;
  254. }
  255. .switch.disabled label {
  256. cursor: not-allowed;
  257. }
  258. .switch label:before {
  259. content: '';
  260. position: absolute;
  261. top: 0;
  262. left: 0;
  263. right: 0;
  264. bottom: 0;
  265. border: 1px solid ${p => p.theme.border};
  266. border-radius: 16px;
  267. }
  268. .switch label:after {
  269. content: '';
  270. width: 10px;
  271. height: 10px;
  272. border-radius: 50%;
  273. transition: all 200ms ease;
  274. background: ${p => p.theme.border};
  275. z-index: 2;
  276. }
  277. .switch input[type='checkbox']:checked + label:after {
  278. background: ${p => p.theme.active};
  279. transform: translateX(16px);
  280. }
  281. `;
  282. export default BaseRRWebReplayer;