transactionBar.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  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. spanBarHatch={false}
  319. style={{
  320. backgroundColor: barColor,
  321. left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
  322. width: toPercent(widthPercentage || 0),
  323. }}
  324. >
  325. <DurationPill
  326. durationDisplay={getDurationDisplay({
  327. left: startPercentage,
  328. width: widthPercentage,
  329. })}
  330. showDetail={showDetail}
  331. spanBarHatch={false}
  332. >
  333. {getHumanDuration(duration)}
  334. </DurationPill>
  335. </RowRectangle>
  336. );
  337. }
  338. renderHeader({
  339. dividerHandlerChildrenProps,
  340. scrollbarManagerChildrenProps,
  341. }: {
  342. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
  343. scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
  344. }) {
  345. const {hasGuideAnchor, index} = this.props;
  346. const {showDetail} = this.state;
  347. const {dividerPosition} = dividerHandlerChildrenProps;
  348. return (
  349. <RowCellContainer showDetail={showDetail}>
  350. <RowCell
  351. data-test-id="transaction-row-title"
  352. data-type="span-row-cell"
  353. style={{
  354. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  355. paddingTop: 0,
  356. }}
  357. showDetail={showDetail}
  358. onClick={this.toggleDisplayDetail}
  359. >
  360. <GuideAnchor target="trace_view_guide_row" disabled={!hasGuideAnchor}>
  361. {this.renderTitle(scrollbarManagerChildrenProps)}
  362. </GuideAnchor>
  363. </RowCell>
  364. <DividerContainer>
  365. {this.renderDivider(dividerHandlerChildrenProps)}
  366. {this.renderErrorBadge()}
  367. </DividerContainer>
  368. <RowCell
  369. data-test-id="transaction-row-duration"
  370. data-type="span-row-cell"
  371. showStriping={index % 2 !== 0}
  372. style={{
  373. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  374. paddingTop: 0,
  375. }}
  376. showDetail={showDetail}
  377. onClick={this.toggleDisplayDetail}
  378. >
  379. <GuideAnchor target="trace_view_guide_row_details" disabled={!hasGuideAnchor}>
  380. {this.renderRectangle()}
  381. </GuideAnchor>
  382. </RowCell>
  383. {!showDetail && this.renderGhostDivider(dividerHandlerChildrenProps)}
  384. </RowCellContainer>
  385. );
  386. }
  387. scrollIntoView = () => {
  388. const element = this.transactionRowDOMRef.current;
  389. if (!element) {
  390. return;
  391. }
  392. const boundingRect = element.getBoundingClientRect();
  393. const offset = boundingRect.top + window.scrollY;
  394. this.setState({showDetail: true}, () => window.scrollTo(0, offset));
  395. };
  396. renderDetail() {
  397. const {location, organization, isVisible, transaction} = this.props;
  398. const {showDetail} = this.state;
  399. return (
  400. <AnchorLinkManager.Consumer>
  401. {({registerScrollFn, scrollToHash}) => {
  402. if (!isTraceFullDetailed(transaction)) {
  403. return null;
  404. }
  405. registerScrollFn(`#txn-${transaction.event_id}`, this.scrollIntoView, false);
  406. if (!isVisible || !showDetail) {
  407. return null;
  408. }
  409. return (
  410. <TransactionDetail
  411. location={location}
  412. organization={organization}
  413. transaction={transaction}
  414. scrollToHash={scrollToHash}
  415. />
  416. );
  417. }}
  418. </AnchorLinkManager.Consumer>
  419. );
  420. }
  421. render() {
  422. const {isVisible, transaction} = this.props;
  423. const {showDetail} = this.state;
  424. return (
  425. <Row
  426. ref={this.transactionRowDOMRef}
  427. visible={isVisible}
  428. showBorder={showDetail}
  429. cursor={isTraceFullDetailed(transaction) ? 'pointer' : 'default'}
  430. >
  431. <ScrollbarManager.Consumer>
  432. {scrollbarManagerChildrenProps => (
  433. <DividerHandlerManager.Consumer>
  434. {dividerHandlerChildrenProps =>
  435. this.renderHeader({
  436. dividerHandlerChildrenProps,
  437. scrollbarManagerChildrenProps,
  438. })
  439. }
  440. </DividerHandlerManager.Consumer>
  441. )}
  442. </ScrollbarManager.Consumer>
  443. {this.renderDetail()}
  444. </Row>
  445. );
  446. }
  447. }
  448. function getOffset(generation) {
  449. return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
  450. }
  451. export default TransactionBar;