spanBar.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import * as React from 'react';
  2. import {withTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import Count from 'app/components/count';
  5. import * as DividerHandlerManager from 'app/components/events/interfaces/spans/dividerHandlerManager';
  6. import {TreeDepthType} from 'app/components/events/interfaces/spans/types';
  7. import {
  8. isOrphanTreeDepth,
  9. unwrapTreeDepth,
  10. } from 'app/components/events/interfaces/spans/utils';
  11. import {ROW_HEIGHT, ROW_PADDING} from 'app/components/performance/waterfall/constants';
  12. import {Row, RowCell, RowCellContainer} from 'app/components/performance/waterfall/row';
  13. import {
  14. DividerLine,
  15. DividerLineGhostContainer,
  16. } from 'app/components/performance/waterfall/rowDivider';
  17. import {RowTitle, RowTitleContainer} from 'app/components/performance/waterfall/rowTitle';
  18. import {
  19. ConnectorBar,
  20. StyledIconChevron,
  21. TOGGLE_BORDER_BOX,
  22. TreeConnector,
  23. TreeToggle,
  24. TreeToggleContainer,
  25. } from 'app/components/performance/waterfall/treeConnector';
  26. import {
  27. getBackgroundColor,
  28. getHatchPattern,
  29. getHumanDuration,
  30. toPercent,
  31. } from 'app/components/performance/waterfall/utils';
  32. import {t} from 'app/locale';
  33. import space from 'app/styles/space';
  34. import {Theme} from 'app/utils/theme';
  35. import SpanDetail from './spanDetail';
  36. import {SpanBarRectangle} from './styles';
  37. import {
  38. DiffSpanType,
  39. generateCSSWidth,
  40. getSpanDescription,
  41. getSpanDuration,
  42. getSpanID,
  43. getSpanOperation,
  44. isOrphanDiffSpan,
  45. SpanGeneratedBoundsType,
  46. } from './utils';
  47. type Props = {
  48. theme: Theme;
  49. span: Readonly<DiffSpanType>;
  50. treeDepth: number;
  51. continuingTreeDepths: Array<TreeDepthType>;
  52. spanNumber: number;
  53. numOfSpanChildren: number;
  54. isRoot: boolean;
  55. isLast: boolean;
  56. showSpanTree: boolean;
  57. toggleSpanTree: () => void;
  58. generateBounds: (span: DiffSpanType) => SpanGeneratedBoundsType;
  59. };
  60. type State = {
  61. showDetail: boolean;
  62. };
  63. class SpanBar extends React.Component<Props, State> {
  64. state: State = {
  65. showDetail: false,
  66. };
  67. renderSpanTreeConnector({hasToggler}: {hasToggler: boolean}) {
  68. const {
  69. isLast,
  70. isRoot,
  71. treeDepth: spanTreeDepth,
  72. continuingTreeDepths,
  73. span,
  74. showSpanTree,
  75. } = this.props;
  76. const spanID = getSpanID(span);
  77. if (isRoot) {
  78. if (hasToggler) {
  79. return (
  80. <ConnectorBar
  81. style={{right: '16px', height: '10px', bottom: '-5px', top: 'auto'}}
  82. key={`${spanID}-last`}
  83. orphanBranch={false}
  84. />
  85. );
  86. }
  87. return null;
  88. }
  89. const connectorBars: Array<React.ReactNode> = continuingTreeDepths.map(treeDepth => {
  90. const depth: number = unwrapTreeDepth(treeDepth);
  91. if (depth === 0) {
  92. // do not render a connector bar at depth 0,
  93. // if we did render a connector bar, this bar would be placed at depth -1
  94. // which does not exist.
  95. return null;
  96. }
  97. const left = ((spanTreeDepth - depth) * (TOGGLE_BORDER_BOX / 2) + 1) * -1;
  98. return (
  99. <ConnectorBar
  100. style={{left}}
  101. key={`${spanID}-${depth}`}
  102. orphanBranch={isOrphanTreeDepth(treeDepth)}
  103. />
  104. );
  105. });
  106. if (hasToggler && showSpanTree) {
  107. // if there is a toggle button, we add a connector bar to create an attachment
  108. // between the toggle button and any connector bars below the toggle button
  109. connectorBars.push(
  110. <ConnectorBar
  111. style={{
  112. right: '16px',
  113. height: '10px',
  114. bottom: isLast ? `-${ROW_HEIGHT / 2}px` : '0',
  115. top: 'auto',
  116. }}
  117. key={`${spanID}-last`}
  118. orphanBranch={false}
  119. />
  120. );
  121. }
  122. return (
  123. <TreeConnector
  124. isLast={isLast}
  125. hasToggler={hasToggler}
  126. orphanBranch={isOrphanDiffSpan(span)}
  127. >
  128. {connectorBars}
  129. </TreeConnector>
  130. );
  131. }
  132. renderSpanTreeToggler({left}: {left: number}) {
  133. const {numOfSpanChildren, isRoot, showSpanTree} = this.props;
  134. const chevron = <StyledIconChevron direction={showSpanTree ? 'up' : 'down'} />;
  135. if (numOfSpanChildren <= 0) {
  136. return (
  137. <TreeToggleContainer style={{left: `${left}px`}}>
  138. {this.renderSpanTreeConnector({hasToggler: false})}
  139. </TreeToggleContainer>
  140. );
  141. }
  142. const chevronElement = !isRoot ? <div>{chevron}</div> : null;
  143. return (
  144. <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
  145. {this.renderSpanTreeConnector({hasToggler: true})}
  146. <TreeToggle
  147. disabled={!!isRoot}
  148. isExpanded={this.props.showSpanTree}
  149. errored={false}
  150. onClick={event => {
  151. event.stopPropagation();
  152. if (isRoot) {
  153. return;
  154. }
  155. this.props.toggleSpanTree();
  156. }}
  157. >
  158. <Count value={numOfSpanChildren} />
  159. {chevronElement}
  160. </TreeToggle>
  161. </TreeToggleContainer>
  162. );
  163. }
  164. renderTitle() {
  165. const {span, treeDepth} = this.props;
  166. const operationName = getSpanOperation(span) ? (
  167. <strong>
  168. {getSpanOperation(span)}
  169. {' \u2014 '}
  170. </strong>
  171. ) : (
  172. ''
  173. );
  174. const description =
  175. getSpanDescription(span) ??
  176. (span.comparisonResult === 'matched' ? t('matched') : getSpanID(span));
  177. const left = treeDepth * (TOGGLE_BORDER_BOX / 2);
  178. return (
  179. <RowTitleContainer>
  180. {this.renderSpanTreeToggler({left})}
  181. <RowTitle
  182. style={{
  183. left: `${left}px`,
  184. width: '100%',
  185. }}
  186. >
  187. <span>
  188. {operationName}
  189. {description}
  190. </span>
  191. </RowTitle>
  192. </RowTitleContainer>
  193. );
  194. }
  195. renderDivider = (
  196. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  197. ) => {
  198. const {theme} = this.props;
  199. if (this.state.showDetail) {
  200. // Mock component to preserve layout spacing
  201. return (
  202. <DividerLine
  203. style={{
  204. position: 'relative',
  205. backgroundColor: getBackgroundColor({
  206. theme,
  207. showDetail: true,
  208. }),
  209. }}
  210. />
  211. );
  212. }
  213. const {addDividerLineRef} = dividerHandlerChildrenProps;
  214. return (
  215. <DividerLine
  216. ref={addDividerLineRef()}
  217. style={{
  218. position: 'relative',
  219. }}
  220. onMouseEnter={() => {
  221. dividerHandlerChildrenProps.setHover(true);
  222. }}
  223. onMouseLeave={() => {
  224. dividerHandlerChildrenProps.setHover(false);
  225. }}
  226. onMouseOver={() => {
  227. dividerHandlerChildrenProps.setHover(true);
  228. }}
  229. onMouseDown={dividerHandlerChildrenProps.onDragStart}
  230. onClick={event => {
  231. // we prevent the propagation of the clicks from this component to prevent
  232. // the span detail from being opened.
  233. event.stopPropagation();
  234. }}
  235. />
  236. );
  237. };
  238. getSpanBarStyles() {
  239. const {theme, span, generateBounds} = this.props;
  240. const bounds = generateBounds(span);
  241. function normalizePadding(width: string | undefined): string | undefined {
  242. if (!width) {
  243. return undefined;
  244. }
  245. return `max(1px, ${width})`;
  246. }
  247. switch (span.comparisonResult) {
  248. case 'matched': {
  249. const baselineDuration = getSpanDuration(span.baselineSpan);
  250. const regressionDuration = getSpanDuration(span.regressionSpan);
  251. if (baselineDuration === regressionDuration) {
  252. return {
  253. background: {
  254. color: undefined,
  255. width: normalizePadding(generateCSSWidth(bounds.background)),
  256. hatch: true,
  257. },
  258. foreground: undefined,
  259. };
  260. }
  261. if (baselineDuration > regressionDuration) {
  262. return {
  263. background: {
  264. // baseline
  265. color: theme.textColor,
  266. width: normalizePadding(generateCSSWidth(bounds.background)),
  267. },
  268. foreground: {
  269. // regression
  270. color: undefined,
  271. width: normalizePadding(generateCSSWidth(bounds.foreground)),
  272. hatch: true,
  273. },
  274. };
  275. }
  276. // case: baselineDuration < regressionDuration
  277. return {
  278. background: {
  279. // regression
  280. color: theme.purple200,
  281. width: normalizePadding(generateCSSWidth(bounds.background)),
  282. },
  283. foreground: {
  284. // baseline
  285. color: undefined,
  286. width: normalizePadding(generateCSSWidth(bounds.foreground)),
  287. hatch: true,
  288. },
  289. };
  290. }
  291. case 'regression': {
  292. return {
  293. background: {
  294. color: theme.purple200,
  295. width: normalizePadding(generateCSSWidth(bounds.background)),
  296. },
  297. foreground: undefined,
  298. };
  299. }
  300. case 'baseline': {
  301. return {
  302. background: {
  303. color: theme.textColor,
  304. width: normalizePadding(generateCSSWidth(bounds.background)),
  305. },
  306. foreground: undefined,
  307. };
  308. }
  309. default: {
  310. const _exhaustiveCheck: never = span;
  311. return _exhaustiveCheck;
  312. }
  313. }
  314. }
  315. renderComparisonReportLabel() {
  316. const {span} = this.props;
  317. switch (span.comparisonResult) {
  318. case 'matched': {
  319. const baselineDuration = getSpanDuration(span.baselineSpan);
  320. const regressionDuration = getSpanDuration(span.regressionSpan);
  321. let label;
  322. if (baselineDuration === regressionDuration) {
  323. label = <ComparisonLabel>{t('No change')}</ComparisonLabel>;
  324. }
  325. if (baselineDuration > regressionDuration) {
  326. const duration = getHumanDuration(
  327. Math.abs(baselineDuration - regressionDuration)
  328. );
  329. label = (
  330. <NotableComparisonLabel>{t('- %s faster', duration)}</NotableComparisonLabel>
  331. );
  332. }
  333. if (baselineDuration < regressionDuration) {
  334. const duration = getHumanDuration(
  335. Math.abs(baselineDuration - regressionDuration)
  336. );
  337. label = (
  338. <NotableComparisonLabel>{t('+ %s slower', duration)}</NotableComparisonLabel>
  339. );
  340. }
  341. return label;
  342. }
  343. case 'baseline': {
  344. return <ComparisonLabel>{t('Only in baseline')}</ComparisonLabel>;
  345. }
  346. case 'regression': {
  347. return <ComparisonLabel>{t('Only in this event')}</ComparisonLabel>;
  348. }
  349. default: {
  350. const _exhaustiveCheck: never = span;
  351. return _exhaustiveCheck;
  352. }
  353. }
  354. }
  355. renderHeader(
  356. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  357. ) {
  358. const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
  359. const {spanNumber, span} = this.props;
  360. const isMatched = span.comparisonResult === 'matched';
  361. const hideSpanBarColumn = this.state.showDetail && isMatched;
  362. const spanBarStyles = this.getSpanBarStyles();
  363. const foregroundSpanBar = spanBarStyles.foreground ? (
  364. <ComparisonSpanBarRectangle
  365. spanBarHatch={spanBarStyles.foreground.hatch ?? false}
  366. style={{
  367. backgroundColor: spanBarStyles.foreground.color,
  368. width: spanBarStyles.foreground.width,
  369. display: hideSpanBarColumn ? 'none' : 'block',
  370. }}
  371. />
  372. ) : null;
  373. return (
  374. <RowCellContainer showDetail={this.state.showDetail}>
  375. <RowCell
  376. data-type="span-row-cell"
  377. showDetail={this.state.showDetail}
  378. style={{
  379. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  380. }}
  381. onClick={() => {
  382. this.toggleDisplayDetail();
  383. }}
  384. >
  385. {this.renderTitle()}
  386. </RowCell>
  387. {this.renderDivider(dividerHandlerChildrenProps)}
  388. <RowCell
  389. data-type="span-row-cell"
  390. showDetail={this.state.showDetail}
  391. showStriping={spanNumber % 2 !== 0}
  392. style={{
  393. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  394. }}
  395. onClick={() => {
  396. this.toggleDisplayDetail();
  397. }}
  398. >
  399. <SpanContainer>
  400. <ComparisonSpanBarRectangle
  401. spanBarHatch={spanBarStyles.background.hatch ?? false}
  402. style={{
  403. backgroundColor: spanBarStyles.background.color,
  404. width: spanBarStyles.background.width,
  405. display: hideSpanBarColumn ? 'none' : 'block',
  406. }}
  407. />
  408. {foregroundSpanBar}
  409. </SpanContainer>
  410. {this.renderComparisonReportLabel()}
  411. </RowCell>
  412. {!this.state.showDetail && (
  413. <DividerLineGhostContainer
  414. style={{
  415. width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  416. display: 'none',
  417. }}
  418. >
  419. <DividerLine
  420. ref={addGhostDividerLineRef()}
  421. style={{
  422. right: 0,
  423. }}
  424. className="hovering"
  425. onClick={event => {
  426. // the ghost divider line should not be interactive.
  427. // we prevent the propagation of the clicks from this component to prevent
  428. // the span detail from being opened.
  429. event.stopPropagation();
  430. }}
  431. />
  432. </DividerLineGhostContainer>
  433. )}
  434. </RowCellContainer>
  435. );
  436. }
  437. toggleDisplayDetail = () => {
  438. this.setState(state => ({
  439. showDetail: !state.showDetail,
  440. }));
  441. };
  442. renderDetail() {
  443. if (!this.state.showDetail) {
  444. return null;
  445. }
  446. const {span, generateBounds} = this.props;
  447. return <SpanDetail span={this.props.span} bounds={generateBounds(span)} />;
  448. }
  449. render() {
  450. return (
  451. <Row visible data-test-id="span-row">
  452. <DividerHandlerManager.Consumer>
  453. {(
  454. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  455. ) => this.renderHeader(dividerHandlerChildrenProps)}
  456. </DividerHandlerManager.Consumer>
  457. {this.renderDetail()}
  458. </Row>
  459. );
  460. }
  461. }
  462. const ComparisonSpanBarRectangle = styled(SpanBarRectangle)<{spanBarHatch: boolean}>`
  463. position: absolute;
  464. left: 0;
  465. height: 16px;
  466. ${p => getHatchPattern(p, p.theme.purple200, p.theme.gray500)}
  467. `;
  468. const ComparisonLabel = styled('div')`
  469. position: absolute;
  470. user-select: none;
  471. right: ${space(1)};
  472. line-height: ${ROW_HEIGHT - 2 * ROW_PADDING}px;
  473. top: ${ROW_PADDING}px;
  474. font-size: ${p => p.theme.fontSizeExtraSmall};
  475. `;
  476. const SpanContainer = styled('div')`
  477. position: relative;
  478. margin-right: 120px;
  479. `;
  480. const NotableComparisonLabel = styled(ComparisonLabel)`
  481. font-weight: bold;
  482. `;
  483. export default withTheme(SpanBar);