replayTimelineEvents.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import {css} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import * as Timeline from 'sentry/components/replays/breadcrumbs/timeline';
  4. import {getCrumbsByColumn} from 'sentry/components/replays/utils';
  5. import Tooltip from 'sentry/components/tooltip';
  6. import space from 'sentry/styles/space';
  7. import {Crumb} from 'sentry/types/breadcrumbs';
  8. import useActiveReplayTab from 'sentry/utils/replays/hooks/useActiveReplayTab';
  9. import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
  10. import type {Color} from 'sentry/utils/theme';
  11. import theme from 'sentry/utils/theme';
  12. import BreadcrumbItem from 'sentry/views/replays/detail/breadcrumbs/breadcrumbItem';
  13. const EVENT_STICK_MARKER_WIDTH = 4;
  14. type Props = {
  15. crumbs: Crumb[];
  16. durationMs: number;
  17. startTimestampMs: number;
  18. width: number;
  19. className?: string;
  20. };
  21. function ReplayTimelineEvents({
  22. className,
  23. crumbs,
  24. durationMs,
  25. startTimestampMs,
  26. width,
  27. }: Props) {
  28. const totalColumns = Math.floor(width / EVENT_STICK_MARKER_WIDTH);
  29. const eventsByCol = getCrumbsByColumn(
  30. startTimestampMs,
  31. durationMs,
  32. crumbs,
  33. totalColumns
  34. );
  35. return (
  36. <Timeline.Columns className={className} totalColumns={totalColumns} remainder={0}>
  37. {Array.from(eventsByCol.entries()).map(([column, breadcrumbs]) => (
  38. <EventColumn key={column} column={column}>
  39. <Event crumbs={breadcrumbs} startTimestampMs={startTimestampMs} />
  40. </EventColumn>
  41. ))}
  42. </Timeline.Columns>
  43. );
  44. }
  45. const EventColumn = styled(Timeline.Col)<{column: number}>`
  46. grid-column: ${p => Math.floor(p.column)};
  47. place-items: stretch;
  48. display: grid;
  49. align-items: center;
  50. position: relative;
  51. &:hover {
  52. z-index: ${p => p.theme.zIndex.initial};
  53. }
  54. `;
  55. function Event({
  56. crumbs,
  57. startTimestampMs,
  58. }: {
  59. crumbs: Crumb[];
  60. startTimestampMs: number;
  61. className?: string;
  62. }) {
  63. const {setActiveTab} = useActiveReplayTab();
  64. const {handleMouseEnter, handleMouseLeave, handleClick} =
  65. useCrumbHandlers(startTimestampMs);
  66. const title = crumbs.map(crumb => (
  67. <BreadcrumbItem
  68. key={crumb.id}
  69. crumb={crumb}
  70. startTimestampMs={startTimestampMs}
  71. isHovered={false}
  72. isSelected={false}
  73. onMouseEnter={handleMouseEnter}
  74. onMouseLeave={handleMouseLeave}
  75. onClick={handleClick}
  76. />
  77. ));
  78. const overlayStyle = css`
  79. /* We make sure to override existing styles */
  80. padding: ${space(0.5)} !important;
  81. max-width: 291px !important;
  82. width: 291px;
  83. @media screen and (max-width: ${theme.breakpoints.small}) {
  84. max-width: 220px !important;
  85. }
  86. `;
  87. // If we have more than 3 events we want to make sure of showing all the different colors that we have
  88. const colors = [...new Set(crumbs.map(crumb => crumb.color))];
  89. // We just need to stack up to 3 times
  90. const totalStackNumber = Math.min(crumbs.length, 3);
  91. // If there is only 1 event use the tab navigation handler on the node
  92. const nodeClickHandler = () => {
  93. if (crumbs.length === 1) {
  94. const crumb = crumbs[0];
  95. switch (crumb.type) {
  96. case 'navigation':
  97. case 'debug':
  98. setActiveTab('network');
  99. break;
  100. case 'ui':
  101. setActiveTab('dom');
  102. break;
  103. case 'error':
  104. default:
  105. setActiveTab('console');
  106. break;
  107. }
  108. }
  109. };
  110. return (
  111. <IconPosition onClick={nodeClickHandler}>
  112. <IconNodeTooltip title={title} overlayStyle={overlayStyle} isHoverable>
  113. {crumbs.slice(0, totalStackNumber).map((crumb, index) => (
  114. <IconNode
  115. color={colors[index] || crumb.color}
  116. key={crumb.id}
  117. stack={{totalStackNumber, index}}
  118. />
  119. ))}
  120. </IconNodeTooltip>
  121. </IconPosition>
  122. );
  123. }
  124. const getNodeDimensions = ({
  125. stack,
  126. }: {
  127. stack: {
  128. index: number;
  129. totalStackNumber: number;
  130. };
  131. }) => {
  132. const {totalStackNumber, index} = stack;
  133. const multiplier = totalStackNumber - index;
  134. const size = (multiplier + 1) * 4;
  135. return `
  136. width: ${size}px;
  137. height: ${size}px;
  138. `;
  139. };
  140. const IconNodeTooltip = styled(Tooltip)`
  141. display: grid;
  142. justify-items: center;
  143. align-items: center;
  144. `;
  145. const IconPosition = styled('div')`
  146. position: absolute;
  147. transform: translate(-50%);
  148. margin-left: ${EVENT_STICK_MARKER_WIDTH / 2}px;
  149. `;
  150. const IconNode = styled('div')<{
  151. color: Color;
  152. stack: {
  153. index: number;
  154. totalStackNumber: number;
  155. };
  156. }>`
  157. grid-column: 1;
  158. grid-row: 1;
  159. ${getNodeDimensions}
  160. border-radius: 50%;
  161. color: ${p => p.theme.white};
  162. background: ${p => p.theme[p.color] ?? p.color};
  163. box-shadow: ${p => p.theme.dropShadowLightest};
  164. user-select: none;
  165. `;
  166. export default ReplayTimelineEvents;