import {Component, createRef, Fragment} from 'react'; import {Location} from 'history'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import Count from 'sentry/components/count'; import * as AnchorLinkManager from 'sentry/components/events/interfaces/spans/anchorLinkManager'; import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager'; import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {ROW_HEIGHT} from 'sentry/components/performance/waterfall/constants'; import { Row, RowCell, RowCellContainer, } from 'sentry/components/performance/waterfall/row'; import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar'; import { DividerContainer, DividerLine, DividerLineGhostContainer, ErrorBadge, } from 'sentry/components/performance/waterfall/rowDivider'; import { RowTitle, RowTitleContainer, RowTitleContent, } from 'sentry/components/performance/waterfall/rowTitle'; import { ConnectorBar, TOGGLE_BORDER_BOX, TreeConnector, TreeToggle, TreeToggleContainer, TreeToggleIcon, } from 'sentry/components/performance/waterfall/treeConnector'; import { getDurationDisplay, getHumanDuration, toPercent, } from 'sentry/components/performance/waterfall/utils'; import Tooltip from 'sentry/components/tooltip'; import {Organization} from 'sentry/types'; import {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types'; import {isTraceFullDetailed} from 'sentry/utils/performance/quickTrace/utils'; import Projects from 'sentry/utils/projects'; import {ProjectBadgeContainer} from './styles'; import TransactionDetail from './transactionDetail'; import {TraceInfo, TraceRoot, TreeDepth} from './types'; const MARGIN_LEFT = 0; type Props = { continuingDepths: TreeDepth[]; hasGuideAnchor: boolean; index: number; isExpanded: boolean; isLast: boolean; isOrphan: boolean; isVisible: boolean; location: Location; organization: Organization; toggleExpandedState: () => void; traceInfo: TraceInfo; transaction: TraceRoot | TraceFullDetailed; barColor?: string; }; type State = { showDetail: boolean; }; class TransactionBar extends Component { state: State = { showDetail: false, }; transactionRowDOMRef = createRef(); toggleDisplayDetail = () => { const {transaction} = this.props; if (isTraceFullDetailed(transaction)) { this.setState(state => ({ showDetail: !state.showDetail, })); } }; getCurrentOffset() { const {transaction} = this.props; const {generation} = transaction; return getOffset(generation); } renderConnector(hasToggle: boolean) { const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = this.props; const {generation} = transaction; const eventId = isTraceFullDetailed(transaction) ? transaction.event_id : transaction.traceSlug; if (generation === 0) { if (hasToggle) { return ( ); } return null; } const connectorBars: Array = continuingDepths.map( ({depth, isOrphanDepth}) => { if (generation - depth <= 1) { // If the difference is less than or equal to 1, then it means that the continued // bar is from its direct parent. In this case, do not render a connector bar // because the tree connector below will suffice. return null; } const left = -1 * getOffset(generation - depth - 1) - 2; return ( ); } ); if (hasToggle && isExpanded) { connectorBars.push( ); } return ( {connectorBars} ); } renderToggle(errored: boolean) { const {isExpanded, transaction, toggleExpandedState} = this.props; const {children, generation} = transaction; const left = this.getCurrentOffset(); if (children.length <= 0) { return ( {this.renderConnector(false)} ); } const isRoot = generation === 0; return ( {this.renderConnector(true)} { event.stopPropagation(); if (isRoot) { return; } toggleExpandedState(); }} > {!isRoot && (
)}
); } renderTitle( scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps ) { const {generateContentSpanBarRef} = scrollbarManagerChildrenProps; const {organization, transaction} = this.props; const left = this.getCurrentOffset(); const errored = isTraceFullDetailed(transaction) ? transaction.errors.length > 0 : false; const content = isTraceFullDetailed(transaction) ? ( {({projects}) => { const project = projects.find(p => p.slug === transaction.project_slug); return ( ); }} {transaction['transaction.op']} {' \u2014 '} {transaction.transaction} ) : ( {'Trace \u2014 '} {transaction.traceSlug} ); return ( {this.renderToggle(errored)} {content} ); } renderDivider( dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps ) { if (this.state.showDetail) { // Mock component to preserve layout spacing return ( ); } const {addDividerLineRef} = dividerHandlerChildrenProps; return ( { 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(); }} /> ); } renderGhostDivider( dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps ) { const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps; return ( { // 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(); }} /> ); } renderErrorBadge() { const {transaction} = this.props; if (!isTraceFullDetailed(transaction) || !transaction.errors.length) { return null; } return ; } renderRectangle() { const {transaction, traceInfo, barColor} = this.props; const {showDetail} = this.state; // Use 1 as the difference in the event that startTimestamp === endTimestamp const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1; const startPosition = Math.abs( transaction.start_timestamp - traceInfo.startTimestamp ); const startPercentage = startPosition / delta; const duration = Math.abs(transaction.timestamp - transaction.start_timestamp); const widthPercentage = duration / delta; return ( {getHumanDuration(duration)} ); } renderHeader({ dividerHandlerChildrenProps, scrollbarManagerChildrenProps, }: { dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps; scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps; }) { const {hasGuideAnchor, index} = this.props; const {showDetail} = this.state; const {dividerPosition} = dividerHandlerChildrenProps; return ( {this.renderTitle(scrollbarManagerChildrenProps)} {this.renderDivider(dividerHandlerChildrenProps)} {this.renderErrorBadge()} {this.renderRectangle()} {!showDetail && this.renderGhostDivider(dividerHandlerChildrenProps)} ); } scrollIntoView = () => { const element = this.transactionRowDOMRef.current; if (!element) { return; } const boundingRect = element.getBoundingClientRect(); const offset = boundingRect.top + window.scrollY; this.setState({showDetail: true}, () => window.scrollTo(0, offset)); }; renderDetail() { const {location, organization, isVisible, transaction} = this.props; const {showDetail} = this.state; return ( {({registerScrollFn, scrollToHash}) => { if (!isTraceFullDetailed(transaction)) { return null; } registerScrollFn(`#txn-${transaction.event_id}`, this.scrollIntoView, false); if (!isVisible || !showDetail) { return null; } return ( ); }} ); } render() { const {isVisible, transaction} = this.props; const {showDetail} = this.state; return ( {scrollbarManagerChildrenProps => ( {dividerHandlerChildrenProps => this.renderHeader({ dividerHandlerChildrenProps, scrollbarManagerChildrenProps, }) } )} {this.renderDetail()} ); } } function getOffset(generation) { return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT; } export default TransactionBar;