transactionBar.tsx 13 KB

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