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. &:hover {
  50. z-index: ${p => p.theme.zIndex.initial};
  51. }
  52. `;
  53. function Event({
  54. crumbs,
  55. startTimestampMs,
  56. }: {
  57. crumbs: Crumb[];
  58. startTimestampMs: number;
  59. className?: string;
  60. }) {
  61. const {setActiveTab} = useActiveReplayTab();
  62. const {handleMouseEnter, handleMouseLeave, handleClick} =
  63. useCrumbHandlers(startTimestampMs);
  64. const title = crumbs.map(crumb => (
  65. <BreadcrumbItem
  66. key={crumb.id}
  67. crumb={crumb}
  68. startTimestampMs={startTimestampMs}
  69. isHovered={false}
  70. isSelected={false}
  71. onMouseEnter={handleMouseEnter}
  72. onMouseLeave={handleMouseLeave}
  73. onClick={handleClick}
  74. />
  75. ));
  76. const overlayStyle = css`
  77. /* We make sure to override existing styles */
  78. padding: ${space(0.5)} !important;
  79. max-width: 291px !important;
  80. width: 291px;
  81. @media screen and (max-width: ${theme.breakpoints.small}) {
  82. max-width: 220px !important;
  83. }
  84. `;
  85. // If we have more than 3 events we want to make sure of showing all the different colors that we have
  86. const colors = [...new Set(crumbs.map(crumb => crumb.color))];
  87. // We just need to stack up to 3 times
  88. const totalStackNumber = Math.min(crumbs.length, 3);
  89. // If there is only 1 event use the tab navigation handler on the node
  90. const nodeClickHandler = () => {
  91. if (crumbs.length === 1) {
  92. const crumb = crumbs[0];
  93. switch (crumb.type) {
  94. case 'navigation':
  95. case 'debug':
  96. setActiveTab('network');
  97. break;
  98. case 'ui':
  99. setActiveTab('dom');
  100. break;
  101. case 'error':
  102. default:
  103. setActiveTab('console');
  104. break;
  105. }
  106. }
  107. };
  108. return (
  109. <IconPosition onClick={nodeClickHandler}>
  110. <IconNodeTooltip title={title} overlayStyle={overlayStyle} isHoverable>
  111. {crumbs.slice(0, totalStackNumber).map((crumb, index) => (
  112. <IconNode
  113. color={colors[index] || crumb.color}
  114. key={crumb.id}
  115. stack={{totalStackNumber, index}}
  116. />
  117. ))}
  118. </IconNodeTooltip>
  119. </IconPosition>
  120. );
  121. }
  122. const getNodeDimensions = ({
  123. stack,
  124. }: {
  125. stack: {
  126. index: number;
  127. totalStackNumber: number;
  128. };
  129. }) => {
  130. const {totalStackNumber, index} = stack;
  131. const multiplier = totalStackNumber - index;
  132. const size = (multiplier + 1) * 4;
  133. return `
  134. width: ${size}px;
  135. height: ${size}px;
  136. `;
  137. };
  138. const IconNodeTooltip = styled(Tooltip)`
  139. display: grid;
  140. justify-items: center;
  141. align-items: center;
  142. `;
  143. const IconPosition = styled('div')`
  144. position: absolute;
  145. transform: translate(-50%);
  146. margin-left: ${EVENT_STICK_MARKER_WIDTH / 2}px;
  147. align-self: center;
  148. display: grid;
  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;