123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543 |
- import * as React from 'react';
- import {withTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import Count from 'app/components/count';
- import * as DividerHandlerManager from 'app/components/events/interfaces/spans/dividerHandlerManager';
- import {TreeDepthType} from 'app/components/events/interfaces/spans/types';
- import {
- isOrphanTreeDepth,
- unwrapTreeDepth,
- } from 'app/components/events/interfaces/spans/utils';
- import {ROW_HEIGHT, ROW_PADDING} from 'app/components/performance/waterfall/constants';
- import {Row, RowCell, RowCellContainer} from 'app/components/performance/waterfall/row';
- import {
- DividerLine,
- DividerLineGhostContainer,
- } from 'app/components/performance/waterfall/rowDivider';
- import {RowTitle, RowTitleContainer} from 'app/components/performance/waterfall/rowTitle';
- import {
- ConnectorBar,
- StyledIconChevron,
- TOGGLE_BORDER_BOX,
- TreeConnector,
- TreeToggle,
- TreeToggleContainer,
- } from 'app/components/performance/waterfall/treeConnector';
- import {
- getBackgroundColor,
- getHatchPattern,
- getHumanDuration,
- toPercent,
- } from 'app/components/performance/waterfall/utils';
- import {t} from 'app/locale';
- import space from 'app/styles/space';
- import {Theme} from 'app/utils/theme';
- import SpanDetail from './spanDetail';
- import {SpanBarRectangle} from './styles';
- import {
- DiffSpanType,
- generateCSSWidth,
- getSpanDescription,
- getSpanDuration,
- getSpanID,
- getSpanOperation,
- isOrphanDiffSpan,
- SpanGeneratedBoundsType,
- } from './utils';
- type Props = {
- theme: Theme;
- span: Readonly<DiffSpanType>;
- treeDepth: number;
- continuingTreeDepths: Array<TreeDepthType>;
- spanNumber: number;
- numOfSpanChildren: number;
- isRoot: boolean;
- isLast: boolean;
- showSpanTree: boolean;
- toggleSpanTree: () => void;
- generateBounds: (span: DiffSpanType) => SpanGeneratedBoundsType;
- };
- type State = {
- showDetail: boolean;
- };
- class SpanBar extends React.Component<Props, State> {
- state: State = {
- showDetail: false,
- };
- renderSpanTreeConnector({hasToggler}: {hasToggler: boolean}) {
- const {
- isLast,
- isRoot,
- treeDepth: spanTreeDepth,
- continuingTreeDepths,
- span,
- showSpanTree,
- } = this.props;
- const spanID = getSpanID(span);
- if (isRoot) {
- if (hasToggler) {
- return (
- <ConnectorBar
- style={{right: '16px', height: '10px', bottom: '-5px', top: 'auto'}}
- key={`${spanID}-last`}
- orphanBranch={false}
- />
- );
- }
- return null;
- }
- const connectorBars: Array<React.ReactNode> = continuingTreeDepths.map(treeDepth => {
- const depth: number = unwrapTreeDepth(treeDepth);
- if (depth === 0) {
- // do not render a connector bar at depth 0,
- // if we did render a connector bar, this bar would be placed at depth -1
- // which does not exist.
- return null;
- }
- const left = ((spanTreeDepth - depth) * (TOGGLE_BORDER_BOX / 2) + 1) * -1;
- return (
- <ConnectorBar
- style={{left}}
- key={`${spanID}-${depth}`}
- orphanBranch={isOrphanTreeDepth(treeDepth)}
- />
- );
- });
- if (hasToggler && showSpanTree) {
- // if there is a toggle button, we add a connector bar to create an attachment
- // between the toggle button and any connector bars below the toggle button
- connectorBars.push(
- <ConnectorBar
- style={{
- right: '16px',
- height: '10px',
- bottom: isLast ? `-${ROW_HEIGHT / 2}px` : '0',
- top: 'auto',
- }}
- key={`${spanID}-last`}
- orphanBranch={false}
- />
- );
- }
- return (
- <TreeConnector
- isLast={isLast}
- hasToggler={hasToggler}
- orphanBranch={isOrphanDiffSpan(span)}
- >
- {connectorBars}
- </TreeConnector>
- );
- }
- renderSpanTreeToggler({left}: {left: number}) {
- const {numOfSpanChildren, isRoot, showSpanTree} = this.props;
- const chevron = <StyledIconChevron direction={showSpanTree ? 'up' : 'down'} />;
- if (numOfSpanChildren <= 0) {
- return (
- <TreeToggleContainer style={{left: `${left}px`}}>
- {this.renderSpanTreeConnector({hasToggler: false})}
- </TreeToggleContainer>
- );
- }
- const chevronElement = !isRoot ? <div>{chevron}</div> : null;
- return (
- <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
- {this.renderSpanTreeConnector({hasToggler: true})}
- <TreeToggle
- disabled={!!isRoot}
- isExpanded={this.props.showSpanTree}
- errored={false}
- onClick={event => {
- event.stopPropagation();
- if (isRoot) {
- return;
- }
- this.props.toggleSpanTree();
- }}
- >
- <Count value={numOfSpanChildren} />
- {chevronElement}
- </TreeToggle>
- </TreeToggleContainer>
- );
- }
- renderTitle() {
- const {span, treeDepth} = this.props;
- const operationName = getSpanOperation(span) ? (
- <strong>
- {getSpanOperation(span)}
- {' \u2014 '}
- </strong>
- ) : (
- ''
- );
- const description =
- getSpanDescription(span) ??
- (span.comparisonResult === 'matched' ? t('matched') : getSpanID(span));
- const left = treeDepth * (TOGGLE_BORDER_BOX / 2);
- return (
- <RowTitleContainer>
- {this.renderSpanTreeToggler({left})}
- <RowTitle
- style={{
- left: `${left}px`,
- width: '100%',
- }}
- >
- <span>
- {operationName}
- {description}
- </span>
- </RowTitle>
- </RowTitleContainer>
- );
- }
- renderDivider = (
- dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
- ) => {
- const {theme} = this.props;
- if (this.state.showDetail) {
- // Mock component to preserve layout spacing
- return (
- <DividerLine
- style={{
- position: 'relative',
- backgroundColor: getBackgroundColor({
- theme,
- showDetail: true,
- }),
- }}
- />
- );
- }
- const {addDividerLineRef} = dividerHandlerChildrenProps;
- return (
- <DividerLine
- ref={addDividerLineRef()}
- style={{
- position: 'relative',
- }}
- onMouseEnter={() => {
- dividerHandlerChildrenProps.setHover(true);
- }}
- onMouseLeave={() => {
- dividerHandlerChildrenProps.setHover(false);
- }}
- onMouseOver={() => {
- dividerHandlerChildrenProps.setHover(true);
- }}
- onMouseDown={dividerHandlerChildrenProps.onDragStart}
- onClick={event => {
- // we prevent the propagation of the clicks from this component to prevent
- // the span detail from being opened.
- event.stopPropagation();
- }}
- />
- );
- };
- getSpanBarStyles() {
- const {theme, span, generateBounds} = this.props;
- const bounds = generateBounds(span);
- function normalizePadding(width: string | undefined): string | undefined {
- if (!width) {
- return undefined;
- }
- return `max(1px, ${width})`;
- }
- switch (span.comparisonResult) {
- case 'matched': {
- const baselineDuration = getSpanDuration(span.baselineSpan);
- const regressionDuration = getSpanDuration(span.regressionSpan);
- if (baselineDuration === regressionDuration) {
- return {
- background: {
- color: undefined,
- width: normalizePadding(generateCSSWidth(bounds.background)),
- hatch: true,
- },
- foreground: undefined,
- };
- }
- if (baselineDuration > regressionDuration) {
- return {
- background: {
- // baseline
- color: theme.textColor,
- width: normalizePadding(generateCSSWidth(bounds.background)),
- },
- foreground: {
- // regression
- color: undefined,
- width: normalizePadding(generateCSSWidth(bounds.foreground)),
- hatch: true,
- },
- };
- }
- // case: baselineDuration < regressionDuration
- return {
- background: {
- // regression
- color: theme.purple200,
- width: normalizePadding(generateCSSWidth(bounds.background)),
- },
- foreground: {
- // baseline
- color: undefined,
- width: normalizePadding(generateCSSWidth(bounds.foreground)),
- hatch: true,
- },
- };
- }
- case 'regression': {
- return {
- background: {
- color: theme.purple200,
- width: normalizePadding(generateCSSWidth(bounds.background)),
- },
- foreground: undefined,
- };
- }
- case 'baseline': {
- return {
- background: {
- color: theme.textColor,
- width: normalizePadding(generateCSSWidth(bounds.background)),
- },
- foreground: undefined,
- };
- }
- default: {
- const _exhaustiveCheck: never = span;
- return _exhaustiveCheck;
- }
- }
- }
- renderComparisonReportLabel() {
- const {span} = this.props;
- switch (span.comparisonResult) {
- case 'matched': {
- const baselineDuration = getSpanDuration(span.baselineSpan);
- const regressionDuration = getSpanDuration(span.regressionSpan);
- let label;
- if (baselineDuration === regressionDuration) {
- label = <ComparisonLabel>{t('No change')}</ComparisonLabel>;
- }
- if (baselineDuration > regressionDuration) {
- const duration = getHumanDuration(
- Math.abs(baselineDuration - regressionDuration)
- );
- label = (
- <NotableComparisonLabel>{t('- %s faster', duration)}</NotableComparisonLabel>
- );
- }
- if (baselineDuration < regressionDuration) {
- const duration = getHumanDuration(
- Math.abs(baselineDuration - regressionDuration)
- );
- label = (
- <NotableComparisonLabel>{t('+ %s slower', duration)}</NotableComparisonLabel>
- );
- }
- return label;
- }
- case 'baseline': {
- return <ComparisonLabel>{t('Only in baseline')}</ComparisonLabel>;
- }
- case 'regression': {
- return <ComparisonLabel>{t('Only in this event')}</ComparisonLabel>;
- }
- default: {
- const _exhaustiveCheck: never = span;
- return _exhaustiveCheck;
- }
- }
- }
- renderHeader(
- dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
- ) {
- const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
- const {spanNumber, span} = this.props;
- const isMatched = span.comparisonResult === 'matched';
- const hideSpanBarColumn = this.state.showDetail && isMatched;
- const spanBarStyles = this.getSpanBarStyles();
- const foregroundSpanBar = spanBarStyles.foreground ? (
- <ComparisonSpanBarRectangle
- spanBarHatch={spanBarStyles.foreground.hatch ?? false}
- style={{
- backgroundColor: spanBarStyles.foreground.color,
- width: spanBarStyles.foreground.width,
- display: hideSpanBarColumn ? 'none' : 'block',
- }}
- />
- ) : null;
- return (
- <RowCellContainer showDetail={this.state.showDetail}>
- <RowCell
- data-type="span-row-cell"
- showDetail={this.state.showDetail}
- style={{
- width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
- }}
- onClick={() => {
- this.toggleDisplayDetail();
- }}
- >
- {this.renderTitle()}
- </RowCell>
- {this.renderDivider(dividerHandlerChildrenProps)}
- <RowCell
- data-type="span-row-cell"
- showDetail={this.state.showDetail}
- showStriping={spanNumber % 2 !== 0}
- style={{
- width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
- }}
- onClick={() => {
- this.toggleDisplayDetail();
- }}
- >
- <SpanContainer>
- <ComparisonSpanBarRectangle
- spanBarHatch={spanBarStyles.background.hatch ?? false}
- style={{
- backgroundColor: spanBarStyles.background.color,
- width: spanBarStyles.background.width,
- display: hideSpanBarColumn ? 'none' : 'block',
- }}
- />
- {foregroundSpanBar}
- </SpanContainer>
- {this.renderComparisonReportLabel()}
- </RowCell>
- {!this.state.showDetail && (
- <DividerLineGhostContainer
- style={{
- width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
- display: 'none',
- }}
- >
- <DividerLine
- ref={addGhostDividerLineRef()}
- style={{
- right: 0,
- }}
- className="hovering"
- onClick={event => {
- // the ghost divider line should not be interactive.
- // we prevent the propagation of the clicks from this component to prevent
- // the span detail from being opened.
- event.stopPropagation();
- }}
- />
- </DividerLineGhostContainer>
- )}
- </RowCellContainer>
- );
- }
- toggleDisplayDetail = () => {
- this.setState(state => ({
- showDetail: !state.showDetail,
- }));
- };
- renderDetail() {
- if (!this.state.showDetail) {
- return null;
- }
- const {span, generateBounds} = this.props;
- return <SpanDetail span={this.props.span} bounds={generateBounds(span)} />;
- }
- render() {
- return (
- <Row visible data-test-id="span-row">
- <DividerHandlerManager.Consumer>
- {(
- dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
- ) => this.renderHeader(dividerHandlerChildrenProps)}
- </DividerHandlerManager.Consumer>
- {this.renderDetail()}
- </Row>
- );
- }
- }
- const ComparisonSpanBarRectangle = styled(SpanBarRectangle)<{spanBarHatch: boolean}>`
- position: absolute;
- left: 0;
- height: 16px;
- ${p => getHatchPattern(p, p.theme.purple200, p.theme.gray500)}
- `;
- const ComparisonLabel = styled('div')`
- position: absolute;
- user-select: none;
- right: ${space(1)};
- line-height: ${ROW_HEIGHT - 2 * ROW_PADDING}px;
- top: ${ROW_PADDING}px;
- font-size: ${p => p.theme.fontSizeExtraSmall};
- `;
- const SpanContainer = styled('div')`
- position: relative;
- margin-right: 120px;
- `;
- const NotableComparisonLabel = styled(ComparisonLabel)`
- font-weight: bold;
- `;
- export default withTheme(SpanBar);
|