transactionBar.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. import {Component, createRef, Fragment} from 'react';
  2. import {Location} from 'history';
  3. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  4. import Count from 'sentry/components/count';
  5. import * as AnchorLinkManager from 'sentry/components/events/interfaces/spans/anchorLinkManager';
  6. import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
  7. import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
  8. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  9. import {ROW_HEIGHT} from 'sentry/components/performance/waterfall/constants';
  10. import {
  11. Row,
  12. RowCell,
  13. RowCellContainer,
  14. } from 'sentry/components/performance/waterfall/row';
  15. import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
  16. import {
  17. DividerContainer,
  18. DividerLine,
  19. DividerLineGhostContainer,
  20. ErrorBadge,
  21. } from 'sentry/components/performance/waterfall/rowDivider';
  22. import {
  23. RowTitle,
  24. RowTitleContainer,
  25. RowTitleContent,
  26. } from 'sentry/components/performance/waterfall/rowTitle';
  27. import {
  28. ConnectorBar,
  29. TOGGLE_BORDER_BOX,
  30. TreeConnector,
  31. TreeToggle,
  32. TreeToggleContainer,
  33. TreeToggleIcon,
  34. } from 'sentry/components/performance/waterfall/treeConnector';
  35. import {
  36. getDurationDisplay,
  37. getHumanDuration,
  38. toPercent,
  39. } from 'sentry/components/performance/waterfall/utils';
  40. import Tooltip from 'sentry/components/tooltip';
  41. import {Organization} from 'sentry/types';
  42. import {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
  43. import {isTraceFullDetailed} from 'sentry/utils/performance/quickTrace/utils';
  44. import Projects from 'sentry/utils/projects';
  45. import {ProjectBadgeContainer} from './styles';
  46. import TransactionDetail from './transactionDetail';
  47. import {TraceInfo, TraceRoot, TreeDepth} from './types';
  48. const MARGIN_LEFT = 0;
  49. type Props = {
  50. continuingDepths: TreeDepth[];
  51. hasGuideAnchor: boolean;
  52. index: number;
  53. isExpanded: boolean;
  54. isLast: boolean;
  55. isOrphan: boolean;
  56. isVisible: boolean;
  57. location: Location;
  58. organization: Organization;
  59. toggleExpandedState: () => void;
  60. traceInfo: TraceInfo;
  61. transaction: TraceRoot | TraceFullDetailed;
  62. barColor?: string;
  63. };
  64. type State = {
  65. showDetail: boolean;
  66. };
  67. class TransactionBar extends Component<Props, State> {
  68. state: State = {
  69. showDetail: false,
  70. };
  71. transactionRowDOMRef = createRef<HTMLDivElement>();
  72. toggleDisplayDetail = () => {
  73. const {transaction} = this.props;
  74. if (isTraceFullDetailed(transaction)) {
  75. this.setState(state => ({
  76. showDetail: !state.showDetail,
  77. }));
  78. }
  79. };
  80. getCurrentOffset() {
  81. const {transaction} = this.props;
  82. const {generation} = transaction;
  83. return getOffset(generation);
  84. }
  85. renderConnector(hasToggle: boolean) {
  86. const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = this.props;
  87. const {generation} = transaction;
  88. const eventId = isTraceFullDetailed(transaction)
  89. ? transaction.event_id
  90. : transaction.traceSlug;
  91. if (generation === 0) {
  92. if (hasToggle) {
  93. return (
  94. <ConnectorBar
  95. style={{right: '15px', height: '10px', bottom: '-5px', top: 'auto'}}
  96. orphanBranch={false}
  97. />
  98. );
  99. }
  100. return null;
  101. }
  102. const connectorBars: Array<React.ReactNode> = continuingDepths.map(
  103. ({depth, isOrphanDepth}) => {
  104. if (generation - depth <= 1) {
  105. // If the difference is less than or equal to 1, then it means that the continued
  106. // bar is from its direct parent. In this case, do not render a connector bar
  107. // because the tree connector below will suffice.
  108. return null;
  109. }
  110. const left = -1 * getOffset(generation - depth - 1) - 2;
  111. return (
  112. <ConnectorBar
  113. style={{left}}
  114. key={`${eventId}-${depth}`}
  115. orphanBranch={isOrphanDepth}
  116. />
  117. );
  118. }
  119. );
  120. if (hasToggle && isExpanded) {
  121. connectorBars.push(
  122. <ConnectorBar
  123. style={{
  124. right: '15px',
  125. height: '10px',
  126. bottom: isLast ? `-${ROW_HEIGHT / 2 + 1}px` : '0',
  127. top: 'auto',
  128. }}
  129. key={`${eventId}-last`}
  130. orphanBranch={false}
  131. />
  132. );
  133. }
  134. return (
  135. <TreeConnector isLast={isLast} hasToggler={hasToggle} orphanBranch={isOrphan}>
  136. {connectorBars}
  137. </TreeConnector>
  138. );
  139. }
  140. renderToggle(errored: boolean) {
  141. const {isExpanded, transaction, toggleExpandedState} = this.props;
  142. const {children, generation} = transaction;
  143. const left = this.getCurrentOffset();
  144. if (children.length <= 0) {
  145. return (
  146. <TreeToggleContainer style={{left: `${left}px`}}>
  147. {this.renderConnector(false)}
  148. </TreeToggleContainer>
  149. );
  150. }
  151. const isRoot = generation === 0;
  152. return (
  153. <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
  154. {this.renderConnector(true)}
  155. <TreeToggle
  156. disabled={isRoot}
  157. isExpanded={isExpanded}
  158. errored={errored}
  159. onClick={event => {
  160. event.stopPropagation();
  161. if (isRoot) {
  162. return;
  163. }
  164. toggleExpandedState();
  165. }}
  166. >
  167. <Count value={children.length} />
  168. {!isRoot && (
  169. <div>
  170. <TreeToggleIcon direction={isExpanded ? 'up' : 'down'} />
  171. </div>
  172. )}
  173. </TreeToggle>
  174. </TreeToggleContainer>
  175. );
  176. }
  177. renderTitle(
  178. scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps
  179. ) {
  180. const {generateContentSpanBarRef} = scrollbarManagerChildrenProps;
  181. const {organization, transaction} = this.props;
  182. const left = this.getCurrentOffset();
  183. const errored = isTraceFullDetailed(transaction)
  184. ? transaction.errors.length > 0
  185. : false;
  186. const content = isTraceFullDetailed(transaction) ? (
  187. <Fragment>
  188. <Projects orgId={organization.slug} slugs={[transaction.project_slug]}>
  189. {({projects}) => {
  190. const project = projects.find(p => p.slug === transaction.project_slug);
  191. return (
  192. <ProjectBadgeContainer>
  193. <Tooltip title={transaction.project_slug}>
  194. <ProjectBadge
  195. project={project ? project : {slug: transaction.project_slug}}
  196. avatarSize={16}
  197. hideName
  198. />
  199. </Tooltip>
  200. </ProjectBadgeContainer>
  201. );
  202. }}
  203. </Projects>
  204. <RowTitleContent errored={errored}>
  205. <strong>
  206. {transaction['transaction.op']}
  207. {' \u2014 '}
  208. </strong>
  209. {transaction.transaction}
  210. </RowTitleContent>
  211. </Fragment>
  212. ) : (
  213. <RowTitleContent errored={false}>
  214. <strong>{'Trace \u2014 '}</strong>
  215. {transaction.traceSlug}
  216. </RowTitleContent>
  217. );
  218. return (
  219. <RowTitleContainer ref={generateContentSpanBarRef()}>
  220. {this.renderToggle(errored)}
  221. <RowTitle
  222. style={{
  223. left: `${left}px`,
  224. width: '100%',
  225. }}
  226. >
  227. {content}
  228. </RowTitle>
  229. </RowTitleContainer>
  230. );
  231. }
  232. renderDivider(
  233. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  234. ) {
  235. if (this.state.showDetail) {
  236. // Mock component to preserve layout spacing
  237. return (
  238. <DividerLine
  239. showDetail
  240. style={{
  241. position: 'absolute',
  242. }}
  243. />
  244. );
  245. }
  246. const {addDividerLineRef} = dividerHandlerChildrenProps;
  247. return (
  248. <DividerLine
  249. ref={addDividerLineRef()}
  250. style={{
  251. position: 'absolute',
  252. }}
  253. onMouseEnter={() => {
  254. dividerHandlerChildrenProps.setHover(true);
  255. }}
  256. onMouseLeave={() => {
  257. dividerHandlerChildrenProps.setHover(false);
  258. }}
  259. onMouseOver={() => {
  260. dividerHandlerChildrenProps.setHover(true);
  261. }}
  262. onMouseDown={dividerHandlerChildrenProps.onDragStart}
  263. onClick={event => {
  264. // we prevent the propagation of the clicks from this component to prevent
  265. // the span detail from being opened.
  266. event.stopPropagation();
  267. }}
  268. />
  269. );
  270. }
  271. renderGhostDivider(
  272. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  273. ) {
  274. const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
  275. return (
  276. <DividerLineGhostContainer
  277. style={{
  278. width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  279. display: 'none',
  280. }}
  281. >
  282. <DividerLine
  283. ref={addGhostDividerLineRef()}
  284. style={{
  285. right: 0,
  286. }}
  287. className="hovering"
  288. onClick={event => {
  289. // the ghost divider line should not be interactive.
  290. // we prevent the propagation of the clicks from this component to prevent
  291. // the span detail from being opened.
  292. event.stopPropagation();
  293. }}
  294. />
  295. </DividerLineGhostContainer>
  296. );
  297. }
  298. renderErrorBadge() {
  299. const {transaction} = this.props;
  300. if (!isTraceFullDetailed(transaction) || !transaction.errors.length) {
  301. return null;
  302. }
  303. return <ErrorBadge />;
  304. }
  305. renderRectangle() {
  306. const {transaction, traceInfo, barColor} = this.props;
  307. const {showDetail} = this.state;
  308. // Use 1 as the difference in the event that startTimestamp === endTimestamp
  309. const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1;
  310. const startPosition = Math.abs(
  311. transaction.start_timestamp - traceInfo.startTimestamp
  312. );
  313. const startPercentage = startPosition / delta;
  314. const duration = Math.abs(transaction.timestamp - transaction.start_timestamp);
  315. const widthPercentage = duration / delta;
  316. return (
  317. <RowRectangle
  318. style={{
  319. backgroundColor: barColor,
  320. left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
  321. width: toPercent(widthPercentage || 0),
  322. }}
  323. >
  324. <DurationPill
  325. durationDisplay={getDurationDisplay({
  326. left: startPercentage,
  327. width: widthPercentage,
  328. })}
  329. showDetail={showDetail}
  330. >
  331. {getHumanDuration(duration)}
  332. </DurationPill>
  333. </RowRectangle>
  334. );
  335. }
  336. renderHeader({
  337. dividerHandlerChildrenProps,
  338. scrollbarManagerChildrenProps,
  339. }: {
  340. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
  341. scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
  342. }) {
  343. const {hasGuideAnchor, index} = this.props;
  344. const {showDetail} = this.state;
  345. const {dividerPosition} = dividerHandlerChildrenProps;
  346. return (
  347. <RowCellContainer showDetail={showDetail}>
  348. <RowCell
  349. data-test-id="transaction-row-title"
  350. data-type="span-row-cell"
  351. style={{
  352. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  353. paddingTop: 0,
  354. }}
  355. showDetail={showDetail}
  356. onClick={this.toggleDisplayDetail}
  357. >
  358. <GuideAnchor target="trace_view_guide_row" disabled={!hasGuideAnchor}>
  359. {this.renderTitle(scrollbarManagerChildrenProps)}
  360. </GuideAnchor>
  361. </RowCell>
  362. <DividerContainer>
  363. {this.renderDivider(dividerHandlerChildrenProps)}
  364. {this.renderErrorBadge()}
  365. </DividerContainer>
  366. <RowCell
  367. data-test-id="transaction-row-duration"
  368. data-type="span-row-cell"
  369. showStriping={index % 2 !== 0}
  370. style={{
  371. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  372. paddingTop: 0,
  373. }}
  374. showDetail={showDetail}
  375. onClick={this.toggleDisplayDetail}
  376. >
  377. <GuideAnchor target="trace_view_guide_row_details" disabled={!hasGuideAnchor}>
  378. {this.renderRectangle()}
  379. </GuideAnchor>
  380. </RowCell>
  381. {!showDetail && this.renderGhostDivider(dividerHandlerChildrenProps)}
  382. </RowCellContainer>
  383. );
  384. }
  385. scrollIntoView = () => {
  386. const element = this.transactionRowDOMRef.current;
  387. if (!element) {
  388. return;
  389. }
  390. const boundingRect = element.getBoundingClientRect();
  391. const offset = boundingRect.top + window.scrollY;
  392. this.setState({showDetail: true}, () => window.scrollTo(0, offset));
  393. };
  394. renderDetail() {
  395. const {location, organization, isVisible, transaction} = this.props;
  396. const {showDetail} = this.state;
  397. return (
  398. <AnchorLinkManager.Consumer>
  399. {({registerScrollFn, scrollToHash}) => {
  400. if (!isTraceFullDetailed(transaction)) {
  401. return null;
  402. }
  403. registerScrollFn(`#txn-${transaction.event_id}`, this.scrollIntoView, false);
  404. if (!isVisible || !showDetail) {
  405. return null;
  406. }
  407. return (
  408. <TransactionDetail
  409. location={location}
  410. organization={organization}
  411. transaction={transaction}
  412. scrollToHash={scrollToHash}
  413. />
  414. );
  415. }}
  416. </AnchorLinkManager.Consumer>
  417. );
  418. }
  419. render() {
  420. const {isVisible, transaction} = this.props;
  421. const {showDetail} = this.state;
  422. return (
  423. <Row
  424. ref={this.transactionRowDOMRef}
  425. visible={isVisible}
  426. showBorder={showDetail}
  427. cursor={isTraceFullDetailed(transaction) ? 'pointer' : 'default'}
  428. >
  429. <ScrollbarManager.Consumer>
  430. {scrollbarManagerChildrenProps => (
  431. <DividerHandlerManager.Consumer>
  432. {dividerHandlerChildrenProps =>
  433. this.renderHeader({
  434. dividerHandlerChildrenProps,
  435. scrollbarManagerChildrenProps,
  436. })
  437. }
  438. </DividerHandlerManager.Consumer>
  439. )}
  440. </ScrollbarManager.Consumer>
  441. {this.renderDetail()}
  442. </Row>
  443. );
  444. }
  445. }
  446. function getOffset(generation) {
  447. return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
  448. }
  449. export default TransactionBar;