Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import SentryTypes from 'app/sentryTypes';
import StacktraceInterface from 'app/components/events/interfaces/stacktrace';
import TemplateInterface from 'app/components/events/interfaces/template';
import ThreadsInterface from 'app/components/events/interfaces/threads';
import SpansInterface from 'app/components/events/interfaces/spans';
import withApi from 'app/utils/withApi';
import withOrganization from 'app/utils/withOrganization';

Expand All @@ -48,6 +49,7 @@ export const INTERFACES = {
breadcrumbs: BreadcrumbsInterface,
threads: ThreadsInterface,
debugmeta: DebugMetaInterface,
spans: SpansInterface,
};

class EventEntries extends React.Component {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React from 'react';

import {rectOfContent, clamp} from './utils';

// we establish the minimum window size so that the window size of 0% is not possible
const MINIMUM_WINDOW_SIZE = 0.5 / 100; // 0.5% window size

enum ViewHandleType {
Left,
Right,
}

export type DragManagerChildrenProps = {
isDragging: boolean;

// left-side handle

onLeftHandleDragStart: (event: React.MouseEvent<SVGRectElement, MouseEvent>) => void;
leftHandlePosition: number; // between 0 to 1
viewWindowStart: number; // between 0 to 1

// right-side handle

onRightHandleDragStart: (event: React.MouseEvent<SVGRectElement, MouseEvent>) => void;
rightHandlePosition: number; // between 0 to 1
viewWindowEnd: number; // between 0 to 1
};

type DragManagerProps = {
children: (props: DragManagerChildrenProps) => JSX.Element;
interactiveLayerRef: React.RefObject<HTMLDivElement>;
};

type DragManagerState = {
isDragging: boolean;
currentDraggingHandle: ViewHandleType | undefined;
leftHandlePosition: number;
rightHandlePosition: number;

viewWindowStart: number;
viewWindowEnd: number;
};

class DragManager extends React.Component<DragManagerProps, DragManagerState> {
state: DragManagerState = {
isDragging: false,
currentDraggingHandle: void 0,
leftHandlePosition: 0, // positioned on the left-most side at 0%
rightHandlePosition: 1, // positioned on the right-most side at 100%

viewWindowStart: 0,
viewWindowEnd: 1,
};

previousUserSelect: string | null = null;

hasInteractiveLayer = (): boolean => {
return !!this.props.interactiveLayerRef.current;
};

onDragStart = (viewHandle: ViewHandleType) => (
event: React.MouseEvent<SVGRectElement, MouseEvent>
) => {
if (
this.state.isDragging ||
event.type !== 'mousedown' ||
!this.hasInteractiveLayer()
) {
return;
}

// prevent the user from selecting things outside the minimap when dragging
// the mouse cursor outside the minimap

this.previousUserSelect = document.body.style.userSelect;
document.body.style.userSelect = 'none';

// attach event listeners so that the mouse cursor can drag outside of the
// minimap
window.addEventListener('mousemove', this.onDragMove);
window.addEventListener('mouseup', this.onDragEnd);

// indicate drag has begun

this.setState({
isDragging: true,
currentDraggingHandle: viewHandle,
});
};

onLeftHandleDragStart = (event: React.MouseEvent<SVGRectElement, MouseEvent>) => {
this.onDragStart(ViewHandleType.Left)(event);
};

onRightHandleDragStart = (event: React.MouseEvent<SVGRectElement, MouseEvent>) => {
this.onDragStart(ViewHandleType.Right)(event);
};

onDragMove = (event: MouseEvent) => {
if (
!this.state.isDragging ||
event.type !== 'mousemove' ||
!this.hasInteractiveLayer()
) {
return;
}

const rect = rectOfContent(this.props.interactiveLayerRef.current!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does the ! do at the end?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It tells the TS compiler that current will is not going to be null/undefined

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ! operator is useful in cases when TS isn't smart enough to know that this.props.interactiveLayerRef.current has been checked to not be null earlier.

more info here: https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#non-null-assertion-operator

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This operator will definitely be useful during the TS migration.


// mouse x-coordinate relative to the interactive layer's left side
const rawMouseX = (event.pageX - rect.x) / rect.width;

switch (this.state.currentDraggingHandle) {
case ViewHandleType.Left: {
const min = 0;
const max = this.state.rightHandlePosition - MINIMUM_WINDOW_SIZE;

this.setState({
// clamp rawMouseX to be within [0, rightHandlePosition - MINIMUM_WINDOW_SIZE]
leftHandlePosition: clamp(rawMouseX, min, max),
});
break;
}
case ViewHandleType.Right: {
const min = this.state.leftHandlePosition + MINIMUM_WINDOW_SIZE;
const max = 1;

this.setState({
// clamp rawMouseX to be within [leftHandlePosition + MINIMUM_WINDOW_SIZE, 1]
rightHandlePosition: clamp(rawMouseX, min, max),
});
break;
}
default: {
throw Error('this.state.currentDraggingHandle is undefined');
}
}
};

onDragEnd = (event: MouseEvent) => {
if (
!this.state.isDragging ||
event.type !== 'mouseup' ||
!this.hasInteractiveLayer()
) {
return;
}

// remove listeners that were attached in onDragStart

window.removeEventListener('mousemove', this.onDragMove);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible this component unmounts before a dragend event happens?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good point 👍

I'm going to consolidate the clean up steps into a function (e.g. restoring the body style) on the next iteration of the APM frontend implementation.

window.removeEventListener('mouseup', this.onDragEnd);

// restore body styles

document.body.style.userSelect = this.previousUserSelect;
this.previousUserSelect = null;

// indicate drag has ended

switch (this.state.currentDraggingHandle) {
case ViewHandleType.Left: {
this.setState(state => {
return {
isDragging: false,
currentDraggingHandle: void 0,

// commit leftHandlePosition to be viewWindowStart
viewWindowStart: state.leftHandlePosition,
};
});
break;
}
case ViewHandleType.Right: {
this.setState(state => {
return {
isDragging: false,
currentDraggingHandle: void 0,

// commit rightHandlePosition to be viewWindowEnd
viewWindowEnd: state.rightHandlePosition,
};
});
break;
}
default: {
throw Error('this.state.currentDraggingHandle is undefined');
}
}

this.setState({
isDragging: false,
currentDraggingHandle: void 0,
});
};

render() {
const childrenProps = {
isDragging: this.state.isDragging,

onLeftHandleDragStart: this.onLeftHandleDragStart,
leftHandlePosition: this.state.leftHandlePosition,
viewWindowStart: this.state.viewWindowStart,

onRightHandleDragStart: this.onRightHandleDragStart,
rightHandlePosition: this.state.rightHandlePosition,
viewWindowEnd: this.state.viewWindowEnd,
};

return this.props.children(childrenProps);
}
}

export default DragManager;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';

import {t} from 'app/locale';
import SentryTypes from 'app/sentryTypes';

import {Panel, PanelHeader, PanelBody} from 'app/components/panels';

import {SpanEntry, SentryEvent} from './types';
import TransactionView from './transactionView';

type SpansInterfacePropTypes = {
event: SentryEvent;
} & SpanEntry;

class SpansInterface extends React.Component<SpansInterfacePropTypes> {
static propTypes = {
event: SentryTypes.Event.isRequired,
};
render() {
const {event} = this.props;

return (
<Panel>
<PanelHeader disablePadding={false} hasButtons={false}>
{t('Trace View')}
</PanelHeader>
<PanelBody>
<TransactionView event={event} />
</PanelBody>
</Panel>
);
}
}

export default SpansInterface;
Loading