|
@@ -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;
|