Files
sentinel/static/app/views/performance/traceDetails/newTraceDetailsTransactionBar.tsx
2024-02-15 17:01:50 +00:00

990 lines
30 KiB
TypeScript

import {createRef, Fragment, useCallback, useEffect, useMemo, useState} from 'react';
import {browserHistory} from 'react-router';
import styled from '@emotion/styled';
import type {Location} from 'history';
import {Observer} from 'mobx-react';
import GuideAnchor from 'sentry/components/assistant/guideAnchor';
import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
import type {SpanDetailProps} from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanDetails';
import NewTraceDetailsSpanTree from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanTree';
import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
import * as SpanContext from 'sentry/components/events/interfaces/spans/spanContext';
import {MeasurementMarker} from 'sentry/components/events/interfaces/spans/styles';
import type {
SpanBoundsType,
SpanGeneratedBoundsType,
VerticalMark,
} from 'sentry/components/events/interfaces/spans/utils';
import {
getMeasurementBounds,
parseTraceDetailsURLHash,
transactionTargetHash,
} from 'sentry/components/events/interfaces/spans/utils';
import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import Link from 'sentry/components/links/link';
import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants';
import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
import {
Row,
RowCell,
RowCellContainer,
RowReplayTimeIndicators,
} from 'sentry/components/performance/waterfall/row';
import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
import {
DividerContainer,
DividerLine,
DividerLineGhostContainer,
ErrorBadge,
MetricsBadge,
} 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,
} from 'sentry/components/performance/waterfall/utils';
import {TransactionProfileIdProvider} from 'sentry/components/profiling/transactionProfileIdProvider';
import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
import {Tooltip} from 'sentry/components/tooltip';
import {IconZoom} from 'sentry/icons/iconZoom';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {EventTransaction, Organization} from 'sentry/types';
import {defined} from 'sentry/utils';
import {hasDDMExperimentalFeature} from 'sentry/utils/metrics/features';
import toPercent from 'sentry/utils/number/toPercent';
import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery';
import type {
TraceError,
TraceFullDetailed,
} from 'sentry/utils/performance/quickTrace/types';
import {
isTraceError,
isTraceRoot,
isTraceTransaction,
} from 'sentry/utils/performance/quickTrace/utils';
import Projects from 'sentry/utils/projects';
import {useApiQuery} from 'sentry/utils/queryClient';
import {decodeScalar} from 'sentry/utils/queryString';
import useRouter from 'sentry/utils/useRouter';
import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider';
import type {EventDetail} from './newTraceDetailsContent';
import {ProjectBadgeContainer} from './styles';
import type {TraceInfo, TraceRoot, TreeDepth} from './types';
import {shortenErrorTitle} from './utils';
const MARGIN_LEFT = 0;
const TRANSACTION_BAR_HEIGHT = 24;
type Props = {
addContentSpanBarRef: (instance: HTMLDivElement | null) => void;
continuingDepths: TreeDepth[];
generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
hasGuideAnchor: boolean;
index: number;
isBarScrolledTo: boolean;
isExpanded: boolean;
isLast: boolean;
isOrphan: boolean;
isVisible: boolean;
location: Location;
onBarScrolledTo: () => void;
onWheel: (deltaX: number) => void;
organization: Organization;
removeContentSpanBarRef: (instance: HTMLDivElement | null) => void;
toggleExpandedState: () => void;
traceInfo: TraceInfo;
traceViewRef: React.RefObject<HTMLDivElement>;
transaction: TraceRoot | TraceFullDetailed | TraceError;
barColor?: string;
isOrphanError?: boolean;
measurements?: Map<number, VerticalMark>;
numOfOrphanErrors?: number;
onRowClick?: (detailKey: EventDetail | SpanDetailProps | undefined) => void;
onlyOrphanErrors?: boolean;
};
function NewTraceDetailsTransactionBar(props: Props) {
const hashValues = parseTraceDetailsURLHash(props.location.hash);
const openPanel = decodeScalar(props.location.query.openPanel);
const eventIDInQueryParam = !!(
isTraceTransaction(props.transaction) &&
hashValues?.eventId &&
hashValues.eventId === props.transaction.event_id
);
const isHighlighted = !!(!hashValues?.spanId && eventIDInQueryParam);
const highlightEmbeddedSpan = !!(hashValues?.spanId && eventIDInQueryParam);
const [showEmbeddedChildren, setShowEmbeddedChildren] = useState(
isHighlighted || highlightEmbeddedSpan
);
const [isIntersecting, setIntersecting] = useState(false);
const transactionRowDOMRef = createRef<HTMLDivElement>();
const transactionTitleRef = createRef<HTMLDivElement>();
let spanContentRef: HTMLDivElement | null = null;
const router = useRouter();
const handleWheel = useCallback(
(event: WheelEvent) => {
// https://stackoverflow.com/q/57358640
// https://github.com/facebook/react/issues/14856
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
return;
}
event.preventDefault();
event.stopPropagation();
if (Math.abs(event.deltaY) === Math.abs(event.deltaX)) {
return;
}
const {onWheel} = props;
onWheel(event.deltaX);
},
[props]
);
const scrollIntoView = useCallback(() => {
const element = transactionRowDOMRef.current;
if (!element) {
return;
}
const boundingRect = element.getBoundingClientRect();
const offset = boundingRect.top + window.scrollY - TRANSACTION_BAR_HEIGHT;
window.scrollTo(0, offset);
props.onBarScrolledTo();
}, [transactionRowDOMRef, props]);
useEffect(() => {
const {transaction, isBarScrolledTo} = props;
const observer = new IntersectionObserver(([entry]) =>
setIntersecting(entry.isIntersecting)
);
if (transactionRowDOMRef.current) {
observer.observe(transactionRowDOMRef.current);
}
if (
'event_id' in transaction &&
hashValues?.eventId === transaction.event_id &&
!isIntersecting &&
!isBarScrolledTo
) {
scrollIntoView();
}
if (isIntersecting) {
props.onBarScrolledTo();
}
return () => {
observer.disconnect();
};
}, [
setIntersecting,
hashValues?.eventId,
hashValues?.spanId,
props,
scrollIntoView,
isIntersecting,
transactionRowDOMRef,
]);
useEffect(() => {
const transactionTitleRefCurrentCopy = transactionTitleRef.current;
if (transactionTitleRefCurrentCopy) {
transactionTitleRefCurrentCopy.addEventListener('wheel', handleWheel, {
passive: false,
});
}
return () => {
if (transactionTitleRefCurrentCopy) {
transactionTitleRefCurrentCopy.removeEventListener('wheel', handleWheel);
}
};
}, [handleWheel, props, transactionTitleRef]);
const transactionEvent =
isTraceTransaction<TraceFullDetailed>(props.transaction) ||
isTraceError(props.transaction)
? props.transaction
: undefined;
const {
data: embeddedChildren,
isLoading: isEmbeddedChildrenLoading,
error: embeddedChildrenError,
} = useApiQuery<EventTransaction>(
[
`/organizations/${props.organization.slug}/events/${transactionEvent?.project_slug}:${transactionEvent?.event_id}/`,
],
{
staleTime: 2 * 60 * 1000,
enabled: showEmbeddedChildren || isHighlighted,
}
);
const waterfallModel = useMemo(() => {
return embeddedChildren
? new WaterfallModel(
embeddedChildren,
undefined,
undefined,
undefined,
props.traceInfo
)
: null;
}, [embeddedChildren, props.traceInfo]);
useEffect(() => {
if (isTraceTransaction(props.transaction) && !isTraceError(props.transaction)) {
if (isHighlighted && props.onRowClick) {
props.onRowClick({
traceFullDetailedEvent: props.transaction,
event: embeddedChildren,
openPanel,
});
}
}
}, [isHighlighted, embeddedChildren, props, props.transaction, openPanel]);
const renderEmbeddedChildrenState = () => {
if (showEmbeddedChildren) {
if (isEmbeddedChildrenLoading) {
return (
<MessageRow>
<span>{t('Loading embedded transaction')}</span>
</MessageRow>
);
}
if (embeddedChildrenError) {
return (
<MessageRow>
<span>{t('Error loading embedded transaction')}</span>
</MessageRow>
);
}
}
return null;
};
const handleRowCellClick = () => {
const {transaction, organization, location} = props;
if (isTraceError(transaction)) {
browserHistory.push(generateIssueEventTarget(transaction, organization));
return;
}
if (isTraceTransaction<TraceFullDetailed>(transaction)) {
router.replace({
...location,
hash: transactionTargetHash(transaction.event_id),
query: {
...location.query,
openPanel: 'open',
},
});
}
};
const getCurrentOffset = () => {
const {transaction} = props;
const {generation} = transaction;
return getOffset(generation);
};
const renderMeasurements = () => {
const {measurements, generateBounds} = props;
if (!measurements) {
return null;
}
return (
<Fragment>
{Array.from(measurements.values()).map(verticalMark => {
const mark = Object.values(verticalMark.marks)[0];
const {timestamp} = mark;
const bounds = getMeasurementBounds(timestamp, generateBounds);
const shouldDisplay = defined(bounds.left) && defined(bounds.width);
if (!shouldDisplay || !bounds.isSpanVisibleInView) {
return null;
}
return (
<MeasurementMarker
key={String(timestamp)}
style={{
left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
}}
failedThreshold={verticalMark.failedThreshold}
/>
);
})}
</Fragment>
);
};
const renderConnector = (hasToggle: boolean) => {
const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = props;
const {generation = 0} = transaction;
const eventId =
isTraceTransaction<TraceFullDetailed>(transaction) || isTraceError(transaction)
? transaction.event_id
: transaction.traceSlug;
if (generation === 0) {
if (hasToggle) {
return (
<ConnectorBar
style={{right: '15px', height: '10px', bottom: '-5px', top: 'auto'}}
orphanBranch={false}
/>
);
}
return null;
}
const connectorBars: Array<React.ReactNode> = 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 (
<ConnectorBar
style={{left}}
key={`${eventId}-${depth}`}
orphanBranch={isOrphanDepth}
/>
);
}
);
const embeddedChildrenLength =
(embeddedChildren && waterfallModel && waterfallModel.rootSpan.children.length) ??
0;
if (
hasToggle &&
(isExpanded || (showEmbeddedChildren && embeddedChildrenLength > 0))
) {
connectorBars.push(
<ConnectorBar
style={{
right: '15px',
height: '10px',
bottom: isLast ? `-${ROW_HEIGHT / 2 + 1}px` : '0',
top: 'auto',
}}
key={`${eventId}-last`}
orphanBranch={false}
/>
);
}
return (
<TreeConnector isLast={isLast} hasToggler={hasToggle} orphanBranch={isOrphan}>
{connectorBars}
</TreeConnector>
);
};
const renderEmbeddedTransactionsBadge = (): React.ReactNode => {
return (
<Tooltip
title={
<span>
{showEmbeddedChildren
? t(
'This transaction is showing a direct child. Remove transaction to hide'
)
: t('This transaction has a direct child. Add transaction to view')}
</span>
}
position="top"
containerDisplayMode="block"
delay={400}
>
<StyledZoomIcon
isZoomIn={!showEmbeddedChildren}
onClick={() => {
setShowEmbeddedChildren(prev => !prev);
if (
(props.isExpanded && !showEmbeddedChildren) ||
(!props.isExpanded && showEmbeddedChildren)
) {
props.toggleExpandedState();
}
}}
/>
</Tooltip>
);
};
const renderEmbeddedChildren = () => {
if (!embeddedChildren || !showEmbeddedChildren || !waterfallModel) {
return null;
}
const {
organization,
traceViewRef,
location,
isLast,
traceInfo,
isExpanded,
toggleExpandedState,
} = props;
const profileId = embeddedChildren.contexts?.profile?.profile_id ?? null;
if (isExpanded) {
toggleExpandedState();
}
return (
<Fragment>
<QuickTraceQuery
event={embeddedChildren}
location={location}
orgSlug={organization.slug}
>
{results => (
<ProfilesProvider
orgSlug={organization.slug}
projectSlug={embeddedChildren.projectSlug ?? ''}
profileId={profileId || ''}
>
<ProfileContext.Consumer>
{profiles => (
<ProfileGroupProvider
type="flamechart"
input={profiles?.type === 'resolved' ? profiles.data : null}
traceID={profileId || ''}
>
<TransactionProfileIdProvider
projectId={embeddedChildren.projectID}
timestamp={embeddedChildren.dateReceived}
transactionId={embeddedChildren.id}
>
<SpanContext.Provider>
<SpanContext.Consumer>
{spanContextProps => (
<Observer>
{() => (
<NewTraceDetailsSpanTree
measurements={props.measurements}
quickTrace={results}
location={props.location}
onRowClick={props.onRowClick}
traceInfo={traceInfo}
traceViewHeaderRef={traceViewRef}
traceViewRef={traceViewRef}
parentContinuingDepths={props.continuingDepths}
traceHasMultipleRoots={props.continuingDepths.some(
c => c.depth === 0 && c.isOrphanDepth
)}
parentIsOrphan={props.isOrphan}
parentIsLast={isLast}
parentGeneration={transaction.generation ?? 0}
organization={organization}
waterfallModel={waterfallModel}
filterSpans={waterfallModel.filterSpans}
spans={waterfallModel
.getWaterfall({
viewStart: 0,
viewEnd: 1,
})
.slice(1)}
focusedSpanIds={waterfallModel.focusedSpanIds}
spanContextProps={spanContextProps}
operationNameFilters={
waterfallModel.operationNameFilters
}
/>
)}
</Observer>
)}
</SpanContext.Consumer>
</SpanContext.Provider>
</TransactionProfileIdProvider>
</ProfileGroupProvider>
)}
</ProfileContext.Consumer>
</ProfilesProvider>
)}
</QuickTraceQuery>
</Fragment>
);
};
const renderToggle = (errored: boolean) => {
const {isExpanded, transaction, toggleExpandedState, numOfOrphanErrors} = props;
const left = getCurrentOffset();
const hasOrphanErrors = numOfOrphanErrors && numOfOrphanErrors > 0;
let childrenLength: number | string =
(!isTraceError(transaction) && transaction.children?.length) || 0;
const generation = transaction.generation || 0;
if (childrenLength <= 0 && !hasOrphanErrors && !showEmbeddedChildren) {
return (
<TreeToggleContainer style={{left: `${left}px`}}>
{renderConnector(false)}
</TreeToggleContainer>
);
}
if (showEmbeddedChildren) {
childrenLength =
embeddedChildren && waterfallModel
? waterfallModel.rootSpan.children.length
: '?';
} else {
childrenLength = childrenLength + (numOfOrphanErrors ?? 0);
}
const isRoot = generation === 0;
return (
<TreeToggleContainer style={{left: `${left}px`}} hasToggler>
{renderConnector(true)}
<TreeToggle
disabled={isRoot}
isExpanded={isExpanded}
errored={errored}
onClick={event => {
event.stopPropagation();
if (isRoot || showEmbeddedChildren) {
return;
}
toggleExpandedState();
setShowEmbeddedChildren(false);
}}
>
<span>{childrenLength}</span>
{!isRoot && !showEmbeddedChildren && (
<div>
<TreeToggleIcon direction={isExpanded ? 'up' : 'down'} />
</div>
)}
</TreeToggle>
</TreeToggleContainer>
);
};
const renderTitle = (_: ScrollbarManager.ScrollbarManagerChildrenProps) => {
const {organization, transaction, addContentSpanBarRef, removeContentSpanBarRef} =
props;
const left = getCurrentOffset();
const errored = isTraceTransaction<TraceFullDetailed>(transaction)
? transaction.errors &&
transaction.errors.length + transaction.performance_issues.length > 0
: false;
const projectBadge = (isTraceTransaction<TraceFullDetailed>(transaction) ||
isTraceError(transaction)) && (
<Projects orgId={organization.slug} slugs={[transaction.project_slug]}>
{({projects}) => {
const project = projects.find(p => p.slug === transaction.project_slug);
return (
<ProjectBadgeContainer>
<Tooltip title={transaction.project_slug}>
<ProjectBadge
project={project ? project : {slug: transaction.project_slug}}
avatarSize={16}
hideName
/>
</Tooltip>
</ProjectBadgeContainer>
);
}}
</Projects>
);
const content = isTraceError(transaction) ? (
<Fragment>
{projectBadge}
<RowTitleContent errored>
<ErrorLink to={generateIssueEventTarget(transaction, organization)}>
<strong>{'Unknown \u2014 '}</strong>
{shortenErrorTitle(transaction.title)}
</ErrorLink>
</RowTitleContent>
</Fragment>
) : isTraceTransaction<TraceFullDetailed>(transaction) ? (
<Fragment>
{projectBadge}
<RowTitleContent errored={errored}>
<strong>
{transaction['transaction.op']}
{' \u2014 '}
</strong>
{transaction.transaction}
</RowTitleContent>
</Fragment>
) : (
<RowTitleContent errored={false}>
<strong>{'Trace \u2014 '}</strong>
{transaction.traceSlug}
</RowTitleContent>
);
return (
<RowTitleContainer
ref={ref => {
if (!ref) {
removeContentSpanBarRef(spanContentRef);
return;
}
addContentSpanBarRef(ref);
spanContentRef = ref;
}}
>
{renderToggle(errored)}
<RowTitle
style={{
left: `${left}px`,
width: '100%',
}}
>
{content}
</RowTitle>
</RowTitleContainer>
);
};
const renderDivider = (
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
) => {
if (isHighlighted) {
// Mock component to preserve layout spacing
return (
<DividerLine
showDetail={isHighlighted}
style={{
position: 'absolute',
}}
/>
);
}
const {addDividerLineRef} = dividerHandlerChildrenProps;
return (
<DividerLine
ref={addDividerLineRef()}
style={{
position: 'absolute',
}}
onMouseEnter={() => {
dividerHandlerChildrenProps.setHover(true);
}}
onMouseLeave={() => {
dividerHandlerChildrenProps.setHover(false);
}}
onMouseOver={() => {
dividerHandlerChildrenProps.setHover(true);
}}
onMouseDown={e => {
dividerHandlerChildrenProps.onDragStart(e);
}}
onClick={event => {
// we prevent the propagation of the clicks from this component to prevent
// the span detail from being opened.
event.stopPropagation();
}}
/>
);
};
const renderGhostDivider = (
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
) => {
const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
return (
<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>
);
};
const renderErrorBadge = () => {
const {transaction} = props;
if (
isTraceRoot(transaction) ||
isTraceError(transaction) ||
!(transaction.errors.length + transaction.performance_issues.length)
) {
return null;
}
return <ErrorBadge />;
};
const renderMetricsBadge = () => {
const {organization} = props;
const hasMetrics = Object.keys(embeddedChildren?._metrics_summary ?? {}).length > 0;
if (
!hasDDMExperimentalFeature(organization) ||
isTraceRoot(transaction) ||
isTraceError(transaction) ||
!hasMetrics
) {
return null;
}
return <MetricsBadge />;
};
const renderRectangle = () => {
const {transaction, traceInfo, barColor} = props;
// Use 1 as the difference in the case that startTimestamp === endTimestamp
const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1;
const start_timestamp = isTraceError(transaction)
? transaction.timestamp
: transaction.start_timestamp;
if (!(start_timestamp && transaction.timestamp)) {
return null;
}
const startPosition = Math.abs(start_timestamp - traceInfo.startTimestamp);
const startPercentage = startPosition / delta;
const duration = Math.abs(transaction.timestamp - start_timestamp);
const widthPercentage = duration / delta;
return (
<StyledRowRectangle
style={{
backgroundColor: barColor,
left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
width: toPercent(widthPercentage || 0),
}}
>
{renderPerformanceIssues()}
{isTraceError(transaction) ? (
<ErrorBadge />
) : (
<Fragment>
{renderMetricsBadge()}
{renderErrorBadge()}
<DurationPill
durationDisplay={getDurationDisplay({
left: startPercentage,
width: widthPercentage,
})}
showDetail={isHighlighted}
>
{getHumanDuration(duration)}
</DurationPill>
</Fragment>
)}
</StyledRowRectangle>
);
};
const renderPerformanceIssues = () => {
const {transaction, barColor} = props;
if (isTraceError(transaction) || isTraceRoot(transaction)) {
return null;
}
const rows: React.ReactElement[] = [];
// Use 1 as the difference in the case that startTimestamp === endTimestamp
const delta = Math.abs(transaction.timestamp - transaction.start_timestamp) || 1;
for (let i = 0; i < transaction.performance_issues.length; i++) {
const issue = transaction.performance_issues[i];
const startPosition = Math.abs(issue.start - transaction.start_timestamp);
const startPercentage = startPosition / delta;
const duration = Math.abs(issue.end - issue.start);
const widthPercentage = duration / delta;
rows.push(
<RowRectangle
style={{
backgroundColor: barColor,
left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
width: toPercent(widthPercentage || 0),
}}
spanBarType={SpanBarType.AFFECTED}
/>
);
}
return rows;
};
const renderHeader = ({
dividerHandlerChildrenProps,
scrollbarManagerChildrenProps,
}: {
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
}) => {
const {hasGuideAnchor, index, transaction, onlyOrphanErrors = false} = props;
const {dividerPosition} = dividerHandlerChildrenProps;
const hideDurationRectangle = isTraceRoot(transaction) && onlyOrphanErrors;
return (
<RowCellContainer showDetail={isHighlighted}>
<RowCell
data-test-id="transaction-row-title"
data-type="span-row-cell"
style={{
width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
paddingTop: 0,
}}
showDetail={isHighlighted}
onClick={handleRowCellClick}
ref={transactionTitleRef}
>
<GuideAnchor target="trace_view_guide_row" disabled={!hasGuideAnchor}>
{renderTitle(scrollbarManagerChildrenProps)}
</GuideAnchor>
</RowCell>
<DividerContainer>
{renderDivider(dividerHandlerChildrenProps)}
{!isTraceRoot(transaction) &&
!isTraceError(transaction) &&
renderEmbeddedTransactionsBadge()}
</DividerContainer>
<RowCell
data-test-id="transaction-row-duration"
data-type="span-row-cell"
showStriping={index % 2 !== 0}
style={{
width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
paddingTop: 0,
overflow: 'visible',
}}
showDetail={isHighlighted}
onClick={handleRowCellClick}
>
<RowReplayTimeIndicators />
<GuideAnchor target="trace_view_guide_row_details" disabled={!hasGuideAnchor}>
{!hideDurationRectangle && renderRectangle()}
{renderMeasurements()}
</GuideAnchor>
</RowCell>
{!isHighlighted && renderGhostDivider(dividerHandlerChildrenProps)}
</RowCellContainer>
);
};
const {isVisible, transaction} = props;
return (
<div>
<StyledRow
ref={transactionRowDOMRef}
visible={isVisible}
showBorder={isHighlighted}
cursor={
isTraceTransaction<TraceFullDetailed>(transaction) ? 'pointer' : 'default'
}
>
<ScrollbarManager.Consumer>
{scrollbarManagerChildrenProps => (
<DividerHandlerManager.Consumer>
{dividerHandlerChildrenProps =>
renderHeader({
dividerHandlerChildrenProps,
scrollbarManagerChildrenProps,
})
}
</DividerHandlerManager.Consumer>
)}
</ScrollbarManager.Consumer>
</StyledRow>
{renderEmbeddedChildrenState()}
{renderEmbeddedChildren()}
</div>
);
}
function getOffset(generation) {
return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
}
export default NewTraceDetailsTransactionBar;
const StyledRow = styled(Row)`
&,
${RowCellContainer} {
overflow: visible;
}
`;
const ErrorLink = styled(Link)`
color: ${p => p.theme.error};
`;
const StyledRowRectangle = styled(RowRectangle)`
display: flex;
align-items: center;
`;
export const StyledZoomIcon = styled(IconZoom)`
position: absolute;
left: -20px;
top: 4px;
height: 16px;
width: 18px;
z-index: 1000;
background: ${p => p.theme.background};
padding: 1px;
border: 1px solid ${p => p.theme.border};
border-radius: ${space(0.5)};
`;