spanGroupBar.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {Fragment, LegacyRef, useEffect, useRef} from 'react';
  2. import Count from 'sentry/components/count';
  3. import {
  4. Row,
  5. RowCell,
  6. RowCellContainer,
  7. } from 'sentry/components/performance/waterfall/row';
  8. import {
  9. DividerContainer,
  10. DividerLine,
  11. DividerLineGhostContainer,
  12. } from 'sentry/components/performance/waterfall/rowDivider';
  13. import {
  14. RowTitle,
  15. RowTitleContainer,
  16. SpanGroupRowTitleContent,
  17. } from 'sentry/components/performance/waterfall/rowTitle';
  18. import {
  19. TOGGLE_BORDER_BOX,
  20. TreeToggle,
  21. TreeToggleContainer,
  22. } from 'sentry/components/performance/waterfall/treeConnector';
  23. import {toPercent} from 'sentry/components/performance/waterfall/utils';
  24. import {EventTransaction} from 'sentry/types/event';
  25. import {defined} from 'sentry/utils';
  26. import * as AnchorLinkManager from './anchorLinkManager';
  27. import * as DividerHandlerManager from './dividerHandlerManager';
  28. import SpanBarCursorGuide from './spanBarCursorGuide';
  29. import {MeasurementMarker} from './styles';
  30. import {EnhancedSpan, ProcessedSpanType} from './types';
  31. import {
  32. getMeasurementBounds,
  33. getMeasurements,
  34. SpanBoundsType,
  35. SpanGeneratedBoundsType,
  36. spanTargetHash,
  37. } from './utils';
  38. const MARGIN_LEFT = 0;
  39. type Props = {
  40. event: Readonly<EventTransaction>;
  41. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
  42. generateContentSpanBarRef: () => (instance: HTMLDivElement | null) => void;
  43. onWheel: (deltaX: number) => void;
  44. renderGroupSpansTitle: () => React.ReactNode;
  45. renderSpanRectangles: () => React.ReactNode;
  46. renderSpanTreeConnector: () => React.ReactNode;
  47. span: Readonly<ProcessedSpanType>;
  48. spanGrouping: EnhancedSpan[];
  49. spanNumber: number;
  50. toggleSpanGroup: () => void;
  51. treeDepth: number;
  52. };
  53. function renderGroupedSpansToggler(props: Props) {
  54. const {treeDepth, spanGrouping, renderSpanTreeConnector, toggleSpanGroup} = props;
  55. const left = treeDepth * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
  56. return (
  57. <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
  58. {renderSpanTreeConnector()}
  59. <TreeToggle
  60. disabled={false}
  61. isExpanded={false}
  62. errored={false}
  63. isSpanGroupToggler
  64. onClick={event => {
  65. event.stopPropagation();
  66. toggleSpanGroup();
  67. }}
  68. >
  69. <Count value={spanGrouping.length} />
  70. </TreeToggle>
  71. </TreeToggleContainer>
  72. );
  73. }
  74. function renderDivider(
  75. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  76. ) {
  77. const {addDividerLineRef} = dividerHandlerChildrenProps;
  78. return (
  79. <DividerLine
  80. ref={addDividerLineRef()}
  81. style={{
  82. position: 'absolute',
  83. }}
  84. onMouseEnter={() => {
  85. dividerHandlerChildrenProps.setHover(true);
  86. }}
  87. onMouseLeave={() => {
  88. dividerHandlerChildrenProps.setHover(false);
  89. }}
  90. onMouseOver={() => {
  91. dividerHandlerChildrenProps.setHover(true);
  92. }}
  93. onMouseDown={dividerHandlerChildrenProps.onDragStart}
  94. onClick={event => {
  95. // we prevent the propagation of the clicks from this component to prevent
  96. // the span detail from being opened.
  97. event.stopPropagation();
  98. }}
  99. />
  100. );
  101. }
  102. function renderMeasurements(
  103. event: Readonly<EventTransaction>,
  104. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  105. ) {
  106. const measurements = getMeasurements(event, generateBounds);
  107. return (
  108. <Fragment>
  109. {Array.from(measurements).map(([timestamp, verticalMark]) => {
  110. const bounds = getMeasurementBounds(timestamp, generateBounds);
  111. const shouldDisplay = defined(bounds.left) && defined(bounds.width);
  112. if (!shouldDisplay || !bounds.isSpanVisibleInView) {
  113. return null;
  114. }
  115. return (
  116. <MeasurementMarker
  117. key={String(timestamp)}
  118. style={{
  119. left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
  120. }}
  121. failedThreshold={verticalMark.failedThreshold}
  122. />
  123. );
  124. })}
  125. </Fragment>
  126. );
  127. }
  128. export function SpanGroupBar(props: Props) {
  129. const spanTitleRef: LegacyRef<HTMLDivElement> | null = useRef(null);
  130. const {onWheel} = props;
  131. useEffect(() => {
  132. const currentRef = spanTitleRef.current;
  133. const handleWheel = (event: WheelEvent) => {
  134. if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
  135. return;
  136. }
  137. event.preventDefault();
  138. event.stopPropagation();
  139. if (Math.abs(event.deltaY) === Math.abs(event.deltaX)) {
  140. return;
  141. }
  142. onWheel(event.deltaX);
  143. };
  144. if (currentRef) {
  145. currentRef.addEventListener('wheel', handleWheel, {
  146. passive: false,
  147. });
  148. }
  149. return () => {
  150. if (currentRef) {
  151. currentRef.removeEventListener('wheel', handleWheel);
  152. }
  153. };
  154. }, [onWheel]);
  155. return (
  156. <DividerHandlerManager.Consumer>
  157. {(
  158. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  159. ) => {
  160. const {
  161. generateBounds,
  162. toggleSpanGroup,
  163. span,
  164. treeDepth,
  165. spanNumber,
  166. event,
  167. spanGrouping,
  168. } = props;
  169. const {isSpanVisibleInView: isSpanVisible} = generateBounds({
  170. startTimestamp: span.start_timestamp,
  171. endTimestamp: span.timestamp,
  172. });
  173. const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
  174. const {generateContentSpanBarRef} = props;
  175. const left = treeDepth * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
  176. return (
  177. <AnchorLinkManager.Consumer>
  178. {({registerScrollFn}) => {
  179. spanGrouping.forEach(spanObj => {
  180. registerScrollFn(
  181. spanTargetHash(spanObj.span.span_id),
  182. toggleSpanGroup,
  183. true
  184. );
  185. });
  186. return (
  187. <Row
  188. visible={isSpanVisible}
  189. showBorder={false}
  190. data-test-id={`span-row-${spanNumber}`}
  191. >
  192. <RowCellContainer>
  193. <RowCell
  194. data-type="span-row-cell"
  195. style={{
  196. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  197. paddingTop: 0,
  198. }}
  199. onClick={() => props.toggleSpanGroup()}
  200. ref={spanTitleRef}
  201. >
  202. <RowTitleContainer ref={generateContentSpanBarRef()}>
  203. {renderGroupedSpansToggler(props)}
  204. <RowTitle
  205. style={{
  206. left: `${left}px`,
  207. width: '100%',
  208. }}
  209. >
  210. <SpanGroupRowTitleContent>
  211. {props.renderGroupSpansTitle()}
  212. </SpanGroupRowTitleContent>
  213. </RowTitle>
  214. </RowTitleContainer>
  215. </RowCell>
  216. <DividerContainer>
  217. {renderDivider(dividerHandlerChildrenProps)}
  218. </DividerContainer>
  219. <RowCell
  220. data-type="span-row-cell"
  221. showStriping={spanNumber % 2 !== 0}
  222. style={{
  223. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  224. }}
  225. onClick={() => props.toggleSpanGroup()}
  226. >
  227. {props.renderSpanRectangles()}
  228. {renderMeasurements(event, generateBounds)}
  229. <SpanBarCursorGuide />
  230. </RowCell>
  231. <DividerLineGhostContainer
  232. style={{
  233. width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  234. display: 'none',
  235. }}
  236. >
  237. <DividerLine
  238. ref={addGhostDividerLineRef()}
  239. style={{
  240. right: 0,
  241. }}
  242. className="hovering"
  243. onClick={e => {
  244. // the ghost divider line should not be interactive.
  245. // we prevent the propagation of the clicks from this component to prevent
  246. // the span detail from being opened.
  247. e.stopPropagation();
  248. }}
  249. />
  250. </DividerLineGhostContainer>
  251. </RowCellContainer>
  252. </Row>
  253. );
  254. }}
  255. </AnchorLinkManager.Consumer>
  256. );
  257. }}
  258. </DividerHandlerManager.Consumer>
  259. );
  260. }