123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- import {
- Fragment,
- LegacyRef,
- MutableRefObject,
- useCallback,
- useEffect,
- useRef,
- } from 'react';
- import Count from 'sentry/components/count';
- import {ROW_HEIGHT} from 'sentry/components/performance/waterfall/constants';
- import {
- Row,
- RowCell,
- RowCellContainer,
- } from 'sentry/components/performance/waterfall/row';
- import {
- DividerContainer,
- DividerLine,
- DividerLineGhostContainer,
- } from 'sentry/components/performance/waterfall/rowDivider';
- import {
- RowTitle,
- RowTitleContainer,
- SpanGroupRowTitleContent,
- } from 'sentry/components/performance/waterfall/rowTitle';
- import {
- TOGGLE_BORDER_BOX,
- TreeToggle,
- TreeToggleContainer,
- } from 'sentry/components/performance/waterfall/treeConnector';
- import {toPercent} from 'sentry/components/performance/waterfall/utils';
- import {EventTransaction} from 'sentry/types/event';
- import {defined} from 'sentry/utils';
- import {PerformanceInteraction} from 'sentry/utils/performanceForSentry';
- import * as DividerHandlerManager from './dividerHandlerManager';
- import SpanBarCursorGuide from './spanBarCursorGuide';
- import {MeasurementMarker} from './styles';
- import {EnhancedSpan, ProcessedSpanType} from './types';
- import {
- getMeasurementBounds,
- getMeasurements,
- SpanBoundsType,
- SpanGeneratedBoundsType,
- spanTargetHash,
- } from './utils';
- const MARGIN_LEFT = 0;
- type Props = {
- addContentSpanBarRef: (instance: HTMLDivElement | null) => void;
- didAnchoredSpanMount: () => boolean;
- event: Readonly<EventTransaction>;
- generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
- getCurrentLeftPos: () => number;
- onWheel: (deltaX: number) => void;
- removeContentSpanBarRef: (instance: HTMLDivElement | null) => void;
- renderGroupSpansTitle: () => React.ReactNode;
- renderSpanRectangles: () => React.ReactNode;
- renderSpanTreeConnector: () => React.ReactNode;
- span: Readonly<ProcessedSpanType>;
- spanGrouping: EnhancedSpan[];
- spanNumber: number;
- toggleSpanGroup: () => void;
- treeDepth: number;
- };
- function renderGroupedSpansToggler(props: Props) {
- const {treeDepth, spanGrouping, renderSpanTreeConnector, toggleSpanGroup} = props;
- const left = treeDepth * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
- return (
- <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
- {renderSpanTreeConnector()}
- <TreeToggle
- disabled={false}
- isExpanded={false}
- errored={false}
- isSpanGroupToggler
- onClick={event => {
- event.stopPropagation();
- toggleSpanGroup();
- }}
- >
- <Count value={spanGrouping.length} />
- </TreeToggle>
- </TreeToggleContainer>
- );
- }
- function renderDivider(
- dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
- ) {
- const {addDividerLineRef} = dividerHandlerChildrenProps;
- return (
- <DividerLine
- ref={addDividerLineRef()}
- style={{
- position: 'absolute',
- }}
- 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();
- }}
- />
- );
- }
- function renderMeasurements(
- event: Readonly<EventTransaction>,
- generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
- ) {
- const measurements = getMeasurements(event, generateBounds);
- return (
- <Fragment>
- {Array.from(measurements).map(([timestamp, verticalMark]) => {
- 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>
- );
- }
- export function SpanGroupBar(props: Props) {
- const spanTitleRef: LegacyRef<HTMLDivElement> | null = useRef(null);
- const spanContentRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
- const {
- onWheel,
- addContentSpanBarRef,
- removeContentSpanBarRef,
- didAnchoredSpanMount,
- spanGrouping,
- toggleSpanGroup,
- getCurrentLeftPos,
- } = props;
- // On mount, it is necessary to set the left styling of the content here due to the span tree being virtualized.
- // If we rely on the scrollBarManager to set the styling, it happens too late and awkwardly applies an animation.
- const setTransformCallback = useCallback(
- (ref: HTMLDivElement | null) => {
- if (ref) {
- spanContentRef.current = ref;
- addContentSpanBarRef(ref);
- const left = -getCurrentLeftPos();
- ref.style.transform = `translateX(${left}px)`;
- ref.style.transformOrigin = 'left';
- return;
- }
- // If ref is null, this means the component is about to unmount
- removeContentSpanBarRef(spanContentRef.current);
- },
- [addContentSpanBarRef, removeContentSpanBarRef, getCurrentLeftPos]
- );
- useEffect(() => {
- if (location.hash && !didAnchoredSpanMount()) {
- const anchoredSpanIndex = spanGrouping.findIndex(
- span => spanTargetHash(span.span.span_id) === location.hash
- );
- // TODO: This doesn't always work.
- // A potential fix is to just scroll to the Autogroup without expanding it if a span within it is anchored.
- if (anchoredSpanIndex > -1) {
- toggleSpanGroup();
- window.scrollTo(0, window.scrollY + anchoredSpanIndex * ROW_HEIGHT);
- }
- }
- }, [didAnchoredSpanMount, spanGrouping, toggleSpanGroup]);
- useEffect(() => {
- const currentRef = spanTitleRef.current;
- const handleWheel = (event: WheelEvent) => {
- if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- if (Math.abs(event.deltaY) === Math.abs(event.deltaX)) {
- return;
- }
- onWheel(event.deltaX);
- };
- if (currentRef) {
- currentRef.addEventListener('wheel', handleWheel, {
- passive: false,
- });
- }
- return () => {
- if (currentRef) {
- currentRef.removeEventListener('wheel', handleWheel);
- }
- };
- }, [onWheel]);
- return (
- <DividerHandlerManager.Consumer>
- {(
- dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
- ) => {
- const {generateBounds, span, treeDepth, spanNumber, event} = props;
- const {isSpanVisibleInView: isSpanVisible} = generateBounds({
- startTimestamp: span.start_timestamp,
- endTimestamp: span.timestamp,
- });
- const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
- const left = treeDepth * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
- return (
- <Row
- visible={isSpanVisible}
- showBorder={false}
- data-test-id={`span-row-${spanNumber}`}
- >
- <RowCellContainer>
- <RowCell
- data-type="span-row-cell"
- style={{
- width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
- paddingTop: 0,
- }}
- onClick={() => {
- PerformanceInteraction.startInteraction('SpanTreeToggle', 1000 * 10);
- props.toggleSpanGroup();
- }}
- ref={spanTitleRef}
- >
- <RowTitleContainer ref={setTransformCallback}>
- {renderGroupedSpansToggler(props)}
- <RowTitle
- style={{
- left: `${left}px`,
- width: '100%',
- }}
- >
- <SpanGroupRowTitleContent>
- {props.renderGroupSpansTitle()}
- </SpanGroupRowTitleContent>
- </RowTitle>
- </RowTitleContainer>
- </RowCell>
- <DividerContainer>
- {renderDivider(dividerHandlerChildrenProps)}
- </DividerContainer>
- <RowCell
- data-type="span-row-cell"
- showStriping={spanNumber % 2 !== 0}
- style={{
- width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
- }}
- onClick={() => toggleSpanGroup()}
- >
- {props.renderSpanRectangles()}
- {renderMeasurements(event, generateBounds)}
- <SpanBarCursorGuide />
- </RowCell>
- <DividerLineGhostContainer
- style={{
- width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
- display: 'none',
- }}
- >
- <DividerLine
- ref={addGhostDividerLineRef()}
- style={{
- right: 0,
- }}
- className="hovering"
- onClick={e => {
- // 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.
- e.stopPropagation();
- }}
- />
- </DividerLineGhostContainer>
- </RowCellContainer>
- </Row>
- );
- }}
- </DividerHandlerManager.Consumer>
- );
- }
|