Browse Source

fix(trace): attach zoom event handler in invisible space (#67946)

Render an invisible row over the entire view and place it under the
"real" rows. This way anytime we have whitespace, the events in that
white space will be dispatched to our invisible row and can be reacted
to. The alternative would have been to detect mouse position on top of
elements, which is going to be more error prone than this.


https://github.com/getsentry/sentry/assets/9317857/0cb1088a-bd98-4d75-9bac-b03a5f0c0afd

---------

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Jonas 11 months ago
parent
commit
590be45800

+ 40 - 17
static/app/views/performance/newTraceDetails/trace.tsx

@@ -618,7 +618,7 @@ export function Trace({
     <TraceStylingWrapper
       ref={r => {
         containerRef.current = r;
-        manager.onContainerRef(r);
+        manager.registerContainerRef(r);
       }}
       className={`${trace.indicators.length > 0 ? 'WithIndicators' : ''} ${trace.type !== 'trace' || scrollQueueRef.current ? 'Loading' : ''}`}
     >
@@ -678,6 +678,16 @@ export function Trace({
       </div>
       <div ref={r => setScrollContainer(r)}>
         <div>{virtualizedList.rendered}</div>
+        <div className="TraceRow Hidden">
+          <div
+            className="TraceLeftColumn"
+            ref={r => manager.registerGhostRowRef('list', r)}
+          />
+          <div
+            className="TraceRightColumn"
+            ref={r => manager.registerGhostRowRef('span_list', r)}
+          />
+        </div>
       </div>
     </TraceStylingWrapper>
   );
@@ -776,7 +786,7 @@ function RenderRow(props: {
         </div>
         <div
           className={
-            props.index % 2 === 0
+            props.index % 2 === 1
               ? RIGHT_COLUMN_ODD_CLASSNAME
               : RIGHT_COLUMN_EVEN_CLASSNAME
           }
@@ -895,7 +905,7 @@ function RenderRow(props: {
             props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
           }
           className={
-            props.index % 2 === 0
+            props.index % 2 === 1
               ? RIGHT_COLUMN_ODD_CLASSNAME
               : RIGHT_COLUMN_EVEN_CLASSNAME
           }
@@ -1010,7 +1020,7 @@ function RenderRow(props: {
             props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
           }
           className={
-            props.index % 2 === 0
+            props.index % 2 === 1
               ? RIGHT_COLUMN_ODD_CLASSNAME
               : RIGHT_COLUMN_EVEN_CLASSNAME
           }
@@ -1085,7 +1095,7 @@ function RenderRow(props: {
             props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
           }
           className={
-            props.index % 2 === 0
+            props.index % 2 === 1
               ? RIGHT_COLUMN_ODD_CLASSNAME
               : RIGHT_COLUMN_EVEN_CLASSNAME
           }
@@ -1094,7 +1104,6 @@ function RenderRow(props: {
             props.manager.onZoomIntoSpace(props.node.space!);
           }}
         >
-          {' '}
           <TraceBar
             virtualized_index={virtualized_index}
             manager={props.manager}
@@ -1150,6 +1159,7 @@ function RenderRow(props: {
               paddingLeft: props.node.depth * props.manager.row_depth_padding,
             }}
           >
+            {' '}
             <div className="TraceChildrenCountWrapper Root">
               <Connectors node={props.node} manager={props.manager} />
               {props.node.children.length > 0 || props.node.canFetch ? (
@@ -1160,7 +1170,6 @@ function RenderRow(props: {
                 </ChildrenButton>
               ) : null}
             </div>
-
             <span className="TraceOperation">{t('Trace')}</span>
             <strong className="TraceEmDash"> — </strong>
             <span className="TraceDescription">{props.trace_id}</span>
@@ -1171,7 +1180,7 @@ function RenderRow(props: {
             props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
           }
           className={
-            props.index % 2 === 0
+            props.index % 2 === 1
               ? RIGHT_COLUMN_ODD_CLASSNAME
               : RIGHT_COLUMN_EVEN_CLASSNAME
           }
@@ -1236,7 +1245,7 @@ function RenderRow(props: {
             }}
           >
             <div className="TraceChildrenCountWrapper">
-              <Connectors node={props.node} manager={props.manager} />
+              <Connectors node={props.node} manager={props.manager} />{' '}
             </div>
             <PlatformIcon
               platform={props.projects[props.node.value.project_slug] ?? 'default'}
@@ -1251,7 +1260,7 @@ function RenderRow(props: {
             props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
           }
           className={
-            props.index % 2 === 0
+            props.index % 2 === 1
               ? RIGHT_COLUMN_ODD_CLASSNAME
               : RIGHT_COLUMN_EVEN_CLASSNAME
           }
@@ -1309,7 +1318,7 @@ function RenderRow(props: {
             <div className="TraceChildrenCountWrapper">
               <Connectors node={props.node} manager={props.manager} />
             </div>
-            <span className="TraceOperation">{t('Empty')}</span>
+            <span className="TraceOperation">{t('Empty')}</span>{' '}
             <strong className="TraceEmDash"> — </strong>
             <span className="TraceDescription">
               {tct('[type] did not report any span data', {
@@ -1329,7 +1338,7 @@ function RenderRow(props: {
             props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
           }
           className={
-            props.index % 2 === 0
+            props.index % 2 === 1
               ? RIGHT_COLUMN_ODD_CLASSNAME
               : RIGHT_COLUMN_EVEN_CLASSNAME
           }
@@ -1400,12 +1409,10 @@ function RenderPlaceholderRow(props: {
       </div>
       <div
         className={
-          props.index % 2 === 0 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME
+          props.index % 2 === 1 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME
         }
         style={{
           width: props.manager.columns.span_list.width * 100 + '%',
-          backgroundColor:
-            props.index % 2 === 0 ? props.theme.backgroundSecondary : undefined,
         }}
       >
         <Placeholder
@@ -2074,6 +2081,20 @@ const TraceStylingWrapper = styled('div')`
     --row-background-hover: ${p => p.theme.translucentSurface100};
     --row-background-focused: ${p => p.theme.translucentSurface200};
 
+    &.Hidden {
+      position: absolute;
+      height: 100%;
+      width: 100%;
+      top: 0;
+      z-index: -1;
+      &:hover {
+        background-color: transparent;
+      }
+      * {
+        cursor: default !important;
+      }
+    }
+
     .TraceError {
       position: absolute;
       top: 50%;
@@ -2225,7 +2246,8 @@ const TraceStylingWrapper = styled('div')`
     will-change: width;
     box-shadow: inset 1px 0 0px 0px transparent;
     cursor: pointer;
-    width: calc(var(--width) * 100%);
+
+    width: calc(var(--list-column-width) * 100%);
 
     .TraceLeftColumnInner {
       height: 100%;
@@ -2252,7 +2274,8 @@ const TraceStylingWrapper = styled('div')`
     will-change: width;
     z-index: 1;
     cursor: pointer;
-    width: calc(var(--width) * 100%);
+
+    width: calc(var(--span-column-width) * 100%);
 
     &:hover {
       .TraceArrow.Visible {

+ 46 - 25
static/app/views/performance/newTraceDetails/virtualizedViewManager.tsx

@@ -298,17 +298,10 @@ export class VirtualizedViewManager {
     this.recomputeSpanToPxMatrix();
   }
 
-  onContainerRef(container: HTMLElement | null) {
-    if (container) {
-      this.initialize(container);
-    } else {
-      this.teardown();
-    }
-  }
-
   dividerScale: 1 | undefined = undefined;
   dividerStartVec: [number, number] | null = null;
   previousDividerClientVec: [number, number] | null = null;
+
   onDividerMouseDown(event: MouseEvent) {
     if (!this.container) {
       return;
@@ -392,6 +385,27 @@ export class VirtualizedViewManager {
     this.draw();
   }
 
+  registerContainerRef(container: HTMLElement | null) {
+    if (container) {
+      this.initialize(container);
+    } else {
+      this.teardown();
+    }
+  }
+
+  registerGhostRowRef(column: string, ref: HTMLElement | null) {
+    if (column === 'list' && ref) {
+      const scrollableElement = ref.children[0] as HTMLElement | undefined;
+      if (scrollableElement) {
+        ref.addEventListener('wheel', this.onSyncedScrollbarScroll, {passive: false});
+      }
+    }
+
+    if (column === 'span_list' && ref) {
+      ref.addEventListener('wheel', this.onWheelZoom, {passive: false});
+    }
+  }
+
   registerList(list: VirtualizedList | null) {
     this.list = list;
   }
@@ -973,6 +987,17 @@ export class VirtualizedViewManager {
 
     this.container = container;
 
+    this.container.style.setProperty(
+      '--list-column-width',
+      // @ts-expect-error we set a number on purpose
+      Math.round(this.columns.list.width * 1000) / 1000
+    );
+    this.container.style.setProperty(
+      '--span-column-width',
+      // @ts-expect-error we set a number on purpose
+      Math.round(this.columns.span_list.width * 1000) / 1000
+    );
+
     this.row_measurer.on('max', this.onNewMaxRowWidth);
 
     this.resize_observer = new ResizeObserver(entries => {
@@ -1322,24 +1347,20 @@ export class VirtualizedViewManager {
       this.indicator_container.style.width = (span_list_width - correction) * 100 + '%';
     }
 
-    for (let i = 0; i < this.columns.list.column_refs.length; i++) {
-      const list = this.columns.list.column_refs[i];
-      if (list) {
-        list.style.setProperty(
-          '--width',
-          // @ts-expect-error we set number value type on purpose
-          Math.round(list_width * 1000) / 1000
-        );
-      }
-      const span = this.columns.span_list.column_refs[i];
-      if (span) {
-        span.style.setProperty(
-          '--width',
-          // @ts-expect-error we set number value type on purpose
-          Math.round(span_list_width * 1000) / 1000
-        );
-      }
+    if (this.container) {
+      this.container.style.setProperty(
+        '--list-column-width',
+        // @ts-expect-error we set number value type on purpose
+        Math.round(list_width * 1000) / 1000
+      );
+      this.container.style.setProperty(
+        '--span-column-width',
+        // @ts-expect-error we set number value type on purpose
+        Math.round(span_list_width * 1000) / 1000
+      );
+    }
 
+    for (let i = 0; i < this.columns.list.column_refs.length; i++) {
       const span_bar = this.span_bars[i];
       const span_arrow = this.span_arrows[i];