traceTimelineEvents.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import color from 'color';
  4. import DateTime from 'sentry/components/dateTime';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import type {Event} from 'sentry/types';
  9. import {TraceTimelineTooltip} from 'sentry/views/issueDetails/traceTimeline/traceTimelineTooltip';
  10. import type {TimelineEvent} from './useTraceTimelineEvents';
  11. import {useTraceTimelineEvents} from './useTraceTimelineEvents';
  12. import {getChunkTimeRange, getEventsByColumn} from './utils';
  13. // Adjusting this will change the number of tooltip groups
  14. const PARENT_WIDTH = 12;
  15. // Adjusting subwidth changes how many dots to render
  16. const CHILD_WIDTH = 4;
  17. interface TraceTimelineEventsProps {
  18. event: Event;
  19. width: number;
  20. }
  21. export function TraceTimelineEvents({event, width}: TraceTimelineEventsProps) {
  22. const {startTimestamp, endTimestamp, data} = useTraceTimelineEvents({event});
  23. let paddedStartTime = startTimestamp;
  24. let paddedEndTime = endTimestamp;
  25. // Duration is 0, pad both sides, this is how we end up with 1 dot in the middle
  26. if (endTimestamp - startTimestamp === 0) {
  27. // If the duration is 0, we need to pad the end time
  28. paddedEndTime = startTimestamp + 1500;
  29. paddedStartTime = startTimestamp - 1500;
  30. }
  31. const durationMs = paddedEndTime - paddedStartTime;
  32. const totalColumns = Math.floor(width / PARENT_WIDTH);
  33. const eventsByColumn = getEventsByColumn(
  34. durationMs,
  35. data,
  36. totalColumns,
  37. paddedStartTime
  38. );
  39. const columnSize = width / totalColumns;
  40. // If the duration is less than 2 minutes, show seconds
  41. const showTimelineSeconds = durationMs < 120 * 1000;
  42. return (
  43. <Fragment>
  44. {/* Add padding to the total columns, 1 column of padding on each side */}
  45. <TimelineColumns style={{gridTemplateColumns: `repeat(${totalColumns + 2}, 1fr)`}}>
  46. {Array.from(eventsByColumn.entries()).map(([column, colEvents]) => {
  47. // Calculate the timestamp range that this column represents
  48. const timeRange = getChunkTimeRange(
  49. paddedStartTime,
  50. column - 1,
  51. durationMs / totalColumns
  52. );
  53. return (
  54. <EventColumn
  55. key={column}
  56. // Add 1 to the column to account for the padding
  57. style={{gridColumn: Math.floor(column) + 1, width: columnSize}}
  58. >
  59. <NodeGroup
  60. event={event}
  61. colEvents={colEvents}
  62. columnSize={columnSize}
  63. timeRange={timeRange}
  64. currentEventId={event.id}
  65. currentColumn={column}
  66. />
  67. </EventColumn>
  68. );
  69. })}
  70. </TimelineColumns>
  71. <TimestampColumns>
  72. <TimestampItem style={{textAlign: 'left'}}>
  73. <DateTime date={paddedStartTime} seconds={showTimelineSeconds} timeOnly />
  74. </TimestampItem>
  75. <TimestampItem style={{textAlign: 'center'}}>
  76. <DateTime
  77. date={paddedStartTime + Math.floor(durationMs / 2)}
  78. seconds={showTimelineSeconds}
  79. timeOnly
  80. />
  81. </TimestampItem>
  82. <TimestampItem style={{textAlign: 'right'}}>
  83. <DateTime date={paddedEndTime} seconds={showTimelineSeconds} timeOnly />
  84. </TimestampItem>
  85. </TimestampColumns>
  86. </Fragment>
  87. );
  88. }
  89. /**
  90. * Use grid to create columns that we can place child nodes into.
  91. * Leveraging grid for alignment means we don't need to calculate percent offset
  92. * nor use position:absolute to lay out items.
  93. *
  94. * <Columns>
  95. * <Col>...</Col>
  96. * <Col>...</Col>
  97. * </Columns>
  98. */
  99. const TimelineColumns = styled('div')`
  100. /* Reset defaults for <ul> */
  101. list-style: none;
  102. margin: 0;
  103. padding: 0;
  104. /* Layout of the lines */
  105. display: grid;
  106. margin-top: -1px;
  107. height: 0;
  108. `;
  109. const TimestampColumns = styled('div')`
  110. display: grid;
  111. grid-template-columns: repeat(3, 1fr);
  112. margin-top: ${space(1)};
  113. `;
  114. const TimestampItem = styled('div')`
  115. place-items: stretch;
  116. display: grid;
  117. align-items: center;
  118. position: relative;
  119. color: ${p => p.theme.subText};
  120. font-size: ${p => p.theme.fontSizeSmall};
  121. `;
  122. function NodeGroup({
  123. event,
  124. timeRange,
  125. colEvents,
  126. columnSize,
  127. currentEventId,
  128. currentColumn,
  129. }: {
  130. colEvents: TimelineEvent[];
  131. columnSize: number;
  132. currentColumn: number;
  133. currentEventId: string;
  134. event: Event;
  135. timeRange: [number, number];
  136. }) {
  137. const totalSubColumns = Math.floor(columnSize / CHILD_WIDTH);
  138. const durationMs = timeRange[1] - timeRange[0];
  139. const eventsByColumn = getEventsByColumn(
  140. durationMs,
  141. colEvents,
  142. totalSubColumns,
  143. timeRange[0]
  144. );
  145. const columns = Array.from(eventsByColumn.keys());
  146. const minColumn = Math.min(...columns);
  147. const maxColumn = Math.max(...columns);
  148. return (
  149. <Fragment>
  150. <TimelineColumns style={{gridTemplateColumns: `repeat(${totalSubColumns}, 1fr)`}}>
  151. {Array.from(eventsByColumn.entries()).map(([column, groupEvents]) => {
  152. const isCurrentNode = groupEvents.some(e => e.id === currentEventId);
  153. return (
  154. <EventColumn key={column} style={{gridColumn: Math.floor(column)}}>
  155. {isCurrentNode && (
  156. <CurrentNodeContainer aria-label={t('Current Event')}>
  157. <CurrentNodeRing />
  158. <CurrentIconNode />
  159. </CurrentNodeContainer>
  160. )}
  161. {!isCurrentNode &&
  162. groupEvents.map(groupEvent =>
  163. 'event.type' in groupEvent ? (
  164. <IconNode key={groupEvent.id} />
  165. ) : (
  166. <PerformanceIconNode key={groupEvent.id} />
  167. )
  168. )}
  169. </EventColumn>
  170. );
  171. })}
  172. </TimelineColumns>
  173. <TimelineColumns style={{gridTemplateColumns: `repeat(${totalSubColumns}, 1fr)`}}>
  174. <Tooltip
  175. title={<TraceTimelineTooltip event={event} timelineEvents={colEvents} />}
  176. overlayStyle={{
  177. padding: `0 !important`,
  178. }}
  179. position="bottom"
  180. isHoverable
  181. skipWrapper
  182. >
  183. <TooltipHelper
  184. style={{
  185. gridColumn: columns.length > 1 ? `${minColumn} / ${maxColumn}` : columns[0],
  186. width: 8 * columns.length,
  187. }}
  188. data-test-id={`trace-timeline-tooltip-${currentColumn}`}
  189. />
  190. </Tooltip>
  191. </TimelineColumns>
  192. </Fragment>
  193. );
  194. }
  195. const EventColumn = styled('div')`
  196. place-items: stretch;
  197. display: grid;
  198. align-items: center;
  199. position: relative;
  200. &:hover {
  201. z-index: ${p => p.theme.zIndex.initial};
  202. }
  203. `;
  204. const IconNode = styled('div')`
  205. position: absolute;
  206. grid-column: 1;
  207. grid-row: 1;
  208. width: 8px;
  209. height: 8px;
  210. border-radius: 50%;
  211. color: ${p => p.theme.white};
  212. box-shadow: ${p => p.theme.dropShadowLight};
  213. user-select: none;
  214. background-color: ${p => color(p.theme.red200).alpha(0.3).string()};
  215. margin-left: -8px;
  216. `;
  217. const PerformanceIconNode = styled(IconNode)`
  218. background-color: unset;
  219. border: 1px solid ${p => color(p.theme.red300).alpha(0.3).string()};
  220. `;
  221. const CurrentNodeContainer = styled('div')`
  222. position: absolute;
  223. grid-column: 1;
  224. grid-row: 1;
  225. width: 8px;
  226. height: 8px;
  227. `;
  228. const CurrentNodeRing = styled('div')`
  229. border: 1px solid ${p => p.theme.red300};
  230. height: 16px;
  231. width: 16px;
  232. border-radius: 100%;
  233. position: absolute;
  234. top: -4px;
  235. left: -12px;
  236. animation: pulse 1s ease-out infinite;
  237. @keyframes pulse {
  238. 0% {
  239. transform: scale(0.1, 0.1);
  240. opacity: 0.0;
  241. }
  242. 50% {
  243. opacity: 1.0;
  244. }
  245. 100% {
  246. transform: scale(1.2, 1.2);
  247. opacity: 0.0;
  248. }
  249. }
  250. `;
  251. const CurrentIconNode = styled(IconNode)`
  252. background-color: ${p => p.theme.red300};
  253. border-radius: 50%;
  254. position: absolute;
  255. `;
  256. const TooltipHelper = styled('span')`
  257. height: 12px;
  258. margin-left: -8px;
  259. margin-top: -6px;
  260. z-index: 1;
  261. `;