Просмотр исходного кода

feat(trace): add minimize functionality to drawer (#67848)

Jonas 11 месяцев назад
Родитель
Сommit
ed9293a583

+ 11 - 7
static/app/utils/useResizableDrawer.tsx

@@ -16,7 +16,11 @@ export interface UseResizableDrawerOptions {
   /**
    * Triggered while dragging
    */
-  onResize: (newSize: number, maybeOldSize?: number) => void;
+  onResize: (
+    newSize: number,
+    maybeOldSize: number | undefined,
+    userEvent: boolean
+  ) => void;
   /**
    * The local storage key used to persist the size of the container
    */
@@ -46,7 +50,7 @@ export function useResizableDrawer(options: UseResizableDrawerOptions): {
   /**
    * Call this function to manually set the size of the drawer.
    */
-  setSize: (newSize: number) => void;
+  setSize: (newSize: number, userEvent?: boolean) => void;
   /**
    * The resulting size of the container axis. Updated while dragging.
    *
@@ -67,9 +71,9 @@ export function useResizableDrawer(options: UseResizableDrawerOptions): {
   const [isHeld, setIsHeld] = useState(false);
 
   const updateSize = useCallback(
-    (newSize: number) => {
+    (newSize: number, userEvent: boolean = false) => {
       setSize(newSize);
-      options.onResize(newSize);
+      options.onResize(newSize, undefined, userEvent);
       if (options.sizeStorageKey) {
         localStorage.setItem(options.sizeStorageKey, newSize.toString());
       }
@@ -81,7 +85,7 @@ export function useResizableDrawer(options: UseResizableDrawerOptions): {
   // any potentional values set by CSS will be overriden. If no initialDimensions are provided,
   // invoke the onResize callback with the previously stored dimensions.
   useLayoutEffect(() => {
-    options.onResize(options.initialSize ?? 0, size);
+    options.onResize(options.initialSize ?? 0, size, false);
     setSize(options.initialSize ?? 0);
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [options.direction]);
@@ -127,7 +131,7 @@ export function useResizableDrawer(options: UseResizableDrawerOptions): {
           Math.max(options.min, sizeRef.current + positionDelta * (isInverted ? -1 : 1))
         );
 
-        updateSize(newSize);
+        updateSize(newSize, true);
       });
     },
     [options.direction, options.min, updateSize]
@@ -154,7 +158,7 @@ export function useResizableDrawer(options: UseResizableDrawerOptions): {
   );
 
   const onDoubleClick = useCallback(() => {
-    updateSize(options.initialSize);
+    updateSize(options.initialSize, true);
   }, [updateSize, options.initialSize]);
 
   useLayoutEffect(() => {

+ 3 - 3
static/app/views/performance/newTraceDetails/index.tsx

@@ -65,10 +65,10 @@ import {
 } from '../../../utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
 import Breadcrumb from '../breadcrumb';
 
-import TraceDrawer from './traceDrawer/traceDrawer';
+import {TraceDrawer} from './traceDrawer/traceDrawer';
 import {isTraceNode} from './guards';
-import Trace from './trace';
-import TraceHeader from './traceHeader';
+import {Trace} from './trace';
+import {TraceHeader} from './traceHeader';
 import {TraceTree, type TraceTreeNode} from './traceTree';
 import {useTrace} from './useTrace';
 import {useTraceMeta} from './useTraceMeta';

+ 1 - 3
static/app/views/performance/newTraceDetails/trace.tsx

@@ -167,7 +167,7 @@ interface TraceProps {
   trace_id: string;
 }
 
-function Trace({
+export function Trace({
   trace,
   trace_id,
   roving_state,
@@ -627,8 +627,6 @@ function Trace({
   );
 }
 
-export default Trace;
-
 function RenderRow(props: {
   index: number;
   isSearchResult: boolean;

+ 156 - 46
static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx

@@ -1,10 +1,10 @@
-import {useCallback, useMemo, useRef} from 'react';
+import {useCallback, useMemo, useRef, useState} from 'react';
 import {type Theme, useTheme} from '@emotion/react';
 import styled from '@emotion/styled';
 import type {Location} from 'history';
 
 import {Button} from 'sentry/components/button';
-import {IconPanel, IconPin} from 'sentry/icons';
+import {IconChevron, IconPanel, IconPin} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {EventTransaction, Organization} from 'sentry/types';
@@ -31,7 +31,7 @@ import {makeTraceNodeBarColor, type TraceTree, type TraceTreeNode} from '../trac
 import NodeDetail from './tabs/details';
 import {TraceLevelDetails} from './tabs/trace';
 
-const MIN_TRACE_DRAWER_DIMENSTIONS: [number, number] = [480, 30];
+const MIN_TRACE_DRAWER_DIMENSTIONS: [number, number] = [480, 27];
 
 type TraceDrawerProps = {
   drawerSize: number;
@@ -50,33 +50,75 @@ type TraceDrawerProps = {
   traces: TraceSplitResults<TraceFullDetailed> | null;
 };
 
-function TraceDrawer(props: TraceDrawerProps) {
+function getUninitializedDrawerSize(layout: TraceDrawerProps['layout']): number {
+  return layout === 'drawer bottom'
+    ? // 36 of the screen height
+      Math.max(window.innerHeight * 0.36)
+    : // Half the screen minus the ~sidebar width
+      Math.max(window.innerWidth * 0.5 - 220, MIN_TRACE_DRAWER_DIMENSTIONS[0]);
+}
+
+function getDrawerInitialSize(
+  layout: TraceDrawerProps['layout'],
+  drawerSize: number
+): number {
+  return drawerSize > 0 ? drawerSize : getUninitializedDrawerSize(layout);
+}
+
+function getDrawerMinSize(layout: TraceDrawerProps['layout']): number {
+  return layout === 'drawer left' || layout === 'drawer right'
+    ? MIN_TRACE_DRAWER_DIMENSTIONS[0]
+    : MIN_TRACE_DRAWER_DIMENSTIONS[1];
+}
+
+const LAYOUT_STORAGE: Partial<Record<TraceDrawerProps['layout'], number>> = {};
+
+export function TraceDrawer(props: TraceDrawerProps) {
   const theme = useTheme();
   const panelRef = useRef<HTMLDivElement>(null);
+  const [minimized, setMinimized] = useState(
+    Math.round(props.drawerSize) <= getDrawerMinSize(props.layout)
+  );
+
+  const minimizedRef = useRef(minimized);
+  minimizedRef.current = minimized;
+
+  const lastNonMinimizedSizeRef =
+    useRef<Partial<Record<TraceDrawerProps['layout'], number>>>(LAYOUT_STORAGE);
+
+  const lastLayoutRef = useRef<TraceDrawerProps['layout']>(props.layout);
 
   const onDrawerResize = props.onDrawerResize;
-  const resizableDrawerOptions: UseResizableDrawerOptions = useMemo(() => {
-    const isSidebarLayout =
-      props.layout === 'drawer left' || props.layout === 'drawer right';
+  const onResize = useCallback(
+    (newSize: number, _oldSize: number | undefined, userEvent: boolean) => {
+      const min = getDrawerMinSize(props.layout);
+
+      // Round to nearest pixel value
+      newSize = Math.round(newSize);
+
+      if (userEvent) {
+        lastNonMinimizedSizeRef.current[props.layout] = newSize;
 
-    const initialSize =
-      props.drawerSize > 0
-        ? props.drawerSize
-        : isSidebarLayout
-          ? // Half the screen minus the ~sidebar width
-            Math.max(window.innerWidth * 0.5 - 220, MIN_TRACE_DRAWER_DIMENSTIONS[0])
-          : // 30% of the screen height
-            Math.max(window.innerHeight * 0.3);
+        // Track the value to see if the user manually minimized or expanded the drawer
+        if (!minimizedRef.current && newSize <= min) {
+          setMinimized(true);
+        } else if (minimizedRef.current && newSize > min) {
+          setMinimized(false);
+        }
+      }
 
-    const min = isSidebarLayout ? window.innerWidth * 0.2 : 30;
+      if (minimizedRef.current) {
+        newSize = min;
+      }
 
-    function onResize(newSize: number) {
       onDrawerResize(newSize);
+      lastLayoutRef.current = props.layout;
+
       if (!panelRef.current) {
         return;
       }
 
-      if (isSidebarLayout) {
+      if (props.layout === 'drawer left' || props.layout === 'drawer right') {
         panelRef.current.style.width = `${newSize}px`;
         panelRef.current.style.height = `100%`;
       } else {
@@ -85,10 +127,16 @@ function TraceDrawer(props: TraceDrawerProps) {
       }
       // @TODO This can visual delays as the rest of the view uses a resize observer
       // to adjust the layout. We should force a sync layout update + draw here to fix that.
-    }
+    },
+    [onDrawerResize, props.layout]
+  );
 
+  const resizableDrawerOptions: UseResizableDrawerOptions = useMemo(() => {
     return {
-      initialSize,
+      initialSize:
+        lastNonMinimizedSizeRef[props.layout] ??
+        getDrawerInitialSize(props.layout, props.drawerSize),
+      min: getDrawerMinSize(props.layout),
       onResize,
       direction:
         props.layout === 'drawer left'
@@ -96,11 +144,39 @@ function TraceDrawer(props: TraceDrawerProps) {
           : props.layout === 'drawer right'
             ? 'right'
             : 'up',
-      min,
     };
-  }, [props.layout, onDrawerResize, props.drawerSize]);
+  }, [props.layout, onResize, props.drawerSize]);
+
+  const {onMouseDown, setSize} = useResizableDrawer(resizableDrawerOptions);
+  const onMinimize = useCallback(
+    (value: boolean) => {
+      minimizedRef.current = value;
+      setMinimized(value);
+
+      if (!value) {
+        const lastUserSize = lastNonMinimizedSizeRef.current[props.layout];
+        const min = getDrawerMinSize(props.layout);
+
+        // If the user has minimized the drawer to the minimum size, we should
+        // restore the drawer to the initial size instead of the last user size.
+        if (lastUserSize === undefined || lastUserSize <= min) {
+          setSize(getUninitializedDrawerSize(props.layout), true);
+          return;
+        }
+
+        setSize(lastUserSize, false);
+        return;
+      }
 
-  const {onMouseDown} = useResizableDrawer(resizableDrawerOptions);
+      setSize(
+        props.layout === 'drawer bottom'
+          ? MIN_TRACE_DRAWER_DIMENSTIONS[1]
+          : MIN_TRACE_DRAWER_DIMENSTIONS[0],
+        false
+      );
+    },
+    [props.layout, setSize]
+  );
 
   const onParentClick = useCallback(
     (node: TraceTreeNode<TraceTree.NodeValue>) => {
@@ -123,6 +199,35 @@ function TraceDrawer(props: TraceDrawerProps) {
           props.trace.indicators.length > 0 && props.layout !== 'drawer bottom'
         }
       >
+        <TabActions>
+          <TabLayoutControlItem>
+            <TabIconButton
+              size="xs"
+              active={minimized}
+              onClick={() => onMinimize(!minimized)}
+              aria-label={t('Minimize')}
+              icon={
+                <SmallerChevronIcon
+                  size="sm"
+                  isCircled
+                  direction={
+                    props.layout === 'drawer bottom'
+                      ? minimized
+                        ? 'up'
+                        : 'down'
+                      : props.layout === 'drawer left'
+                        ? minimized
+                          ? 'right'
+                          : 'left'
+                        : minimized
+                          ? 'left'
+                          : 'right'
+                  }
+                />
+              }
+            />
+          </TabLayoutControlItem>
+        </TabActions>
         <TabsContainer
           style={{
             gridTemplateColumns: `repeat(${props.tabs.tabs.length + (props.tabs.last_clicked ? 1 : 0)}, minmax(0, min-content))`,
@@ -157,38 +262,35 @@ function TraceDrawer(props: TraceDrawerProps) {
             />
           ) : null}
         </TabsContainer>
-        <TabLayoutControlsContainer>
+        <TabActions>
           <TabLayoutControlItem>
-            <DrawerButton
+            <TabIconButton
               active={props.layout === 'drawer left'}
               onClick={() => props.onLayoutChange('drawer left')}
               size="xs"
-              title={t('Drawer left')}
-            >
-              <IconPanel size="xs" direction="left" />
-            </DrawerButton>
+              aria-label={t('Drawer left')}
+              icon={<IconPanel size="xs" direction="left" />}
+            />
           </TabLayoutControlItem>
           <TabLayoutControlItem>
-            <DrawerButton
+            <TabIconButton
               active={props.layout === 'drawer bottom'}
               onClick={() => props.onLayoutChange('drawer bottom')}
               size="xs"
-              title={t('Drawer bottom')}
-            >
-              <IconPanel size="xs" direction="down" />
-            </DrawerButton>
+              aria-label={t('Drawer bottom')}
+              icon={<IconPanel size="xs" direction="down" />}
+            />
           </TabLayoutControlItem>
           <TabLayoutControlItem>
-            <DrawerButton
+            <TabIconButton
               active={props.layout === 'drawer right'}
               onClick={() => props.onLayoutChange('drawer right')}
               size="xs"
-              title={t('Drawer right')}
-            >
-              <IconPanel size="xs" direction="right" />
-            </DrawerButton>
+              aria-label={t('Drawer right')}
+              icon={<IconPanel size="xs" direction="right" />}
+            />
           </TabLayoutControlItem>
-        </TabLayoutControlsContainer>
+        </TabActions>
       </TabsLayout>
       <Content layout={props.layout}>
         <ContentWrapper>
@@ -314,12 +416,21 @@ const PanelWrapper = styled('div')<{
   z-index: 10;
 `;
 
+const SmallerChevronIcon = styled(IconChevron)`
+  width: 13px;
+  height: 13px;
+
+  transition: none;
+`;
+
 const TabsLayout = styled('div')<{hasIndicators: boolean}>`
   display: grid;
-  grid-template-columns: 1fr auto;
+  grid-template-columns: auto 1fr auto;
   border-bottom: 1px solid ${p => p.theme.border};
   background-color: ${p => p.theme.backgroundSecondary};
   height: ${p => (p.hasIndicators ? '44px' : '26px')};
+  padding-left: ${space(0.25)};
+  padding-right: ${space(0.5)};
 `;
 
 const TabsContainer = styled('ul')`
@@ -328,12 +439,12 @@ const TabsContainer = styled('ul')`
   width: 100%;
   align-items: center;
   justify-content: left;
-  padding-left: ${space(1)};
   gap: ${space(1)};
+  padding-left: 0;
   margin-bottom: 0;
 `;
 
-const TabLayoutControlsContainer = styled('ul')`
+const TabActions = styled('ul')`
   list-style-type: none;
   padding-left: 0;
   margin-bottom: 0;
@@ -375,6 +486,7 @@ const TabButtonIndicator = styled('div')<{backgroundColor: string}>`
   border-radius: 2px;
   background-color: ${p => p.backgroundColor};
 `;
+
 const TabButton = styled('button')`
   height: 100%;
   border: none;
@@ -418,7 +530,7 @@ const Content = styled('div')<{layout: 'drawer bottom' | 'drawer left' | 'drawer
       `}
 `;
 
-const DrawerButton = styled(Button)<{active: boolean}>`
+const TabIconButton = styled(Button)<{active: boolean}>`
   border: none;
   background-color: transparent;
   box-shadow: none;
@@ -468,5 +580,3 @@ const ContentWrapper = styled('div')`
   inset: ${space(1)};
   position: absolute;
 `;
-
-export default TraceDrawer;

+ 1 - 1
static/app/views/performance/newTraceDetails/traceHeader.tsx

@@ -79,7 +79,7 @@ type TraceHeaderProps = {
   tree: TraceTree;
 };
 
-export default function TraceHeader({
+export function TraceHeader({
   metaResults,
   rootEventResults,
   traces,