spanGroupBar.tsx 9.7 KB

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