Skip to content

Commit f593680

Browse files
committed
SEN-846, SEN-803, SEN-808: trace view + minimap
1 parent 6ffcece commit f593680

File tree

7 files changed

+1374
-0
lines changed

7 files changed

+1374
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import React from 'react';
2+
3+
import {rectOfContent, clamp} from './utils';
4+
5+
// we establish the minimum window size so that the window size of 0% is not possible
6+
const MINIMUM_WINDOW_SIZE = 0.5 / 100; // 0.5% window size
7+
8+
enum ViewHandleType {
9+
Left,
10+
Right,
11+
}
12+
13+
export type DragManagerChildrenProps = {
14+
isDragging: boolean;
15+
16+
// left-side handle
17+
18+
onLeftHandleDragStart: (event: React.MouseEvent<SVGRectElement, MouseEvent>) => void;
19+
leftHandlePosition: number; // between 0 to 1
20+
viewWindowStart: number; // between 0 to 1
21+
22+
// right-side handle
23+
24+
onRightHandleDragStart: (event: React.MouseEvent<SVGRectElement, MouseEvent>) => void;
25+
rightHandlePosition: number; // between 0 to 1
26+
viewWindowEnd: number; // between 0 to 1
27+
};
28+
29+
type DragManagerProps = {
30+
children: (props: DragManagerChildrenProps) => JSX.Element;
31+
interactiveLayerRef: React.RefObject<HTMLDivElement>;
32+
};
33+
34+
type DragManagerState = {
35+
isDragging: boolean;
36+
currentDraggingHandle: ViewHandleType | undefined;
37+
leftHandlePosition: number;
38+
rightHandlePosition: number;
39+
40+
viewWindowStart: number;
41+
viewWindowEnd: number;
42+
};
43+
44+
class DragManager extends React.Component<DragManagerProps, DragManagerState> {
45+
state: DragManagerState = {
46+
isDragging: false,
47+
currentDraggingHandle: void 0,
48+
leftHandlePosition: 0, // positioned on the left-most side at 0%
49+
rightHandlePosition: 1, // positioned on the right-most side at 100%
50+
51+
viewWindowStart: 0,
52+
viewWindowEnd: 1,
53+
};
54+
55+
previousUserSelect: string | null = null;
56+
57+
hasInteractiveLayer = (): boolean => {
58+
return !!this.props.interactiveLayerRef.current;
59+
};
60+
61+
onDragStart = (viewHandle: ViewHandleType) => (
62+
event: React.MouseEvent<SVGRectElement, MouseEvent>
63+
) => {
64+
if (
65+
this.state.isDragging ||
66+
event.type !== 'mousedown' ||
67+
!this.hasInteractiveLayer()
68+
) {
69+
return;
70+
}
71+
72+
// prevent the user from selecting things outside the minimap when dragging
73+
// the mouse cursor outside the minimap
74+
75+
this.previousUserSelect = document.body.style.userSelect;
76+
document.body.style.userSelect = 'none';
77+
78+
// attach event listeners so that the mouse cursor can drag outside of the
79+
// minimap
80+
window.addEventListener('mousemove', this.onDragMove);
81+
window.addEventListener('mouseup', this.onDragEnd);
82+
83+
// indicate drag has begun
84+
85+
this.setState({
86+
isDragging: true,
87+
currentDraggingHandle: viewHandle,
88+
});
89+
};
90+
91+
onLeftHandleDragStart = (event: React.MouseEvent<SVGRectElement, MouseEvent>) => {
92+
this.onDragStart(ViewHandleType.Left)(event);
93+
};
94+
95+
onRightHandleDragStart = (event: React.MouseEvent<SVGRectElement, MouseEvent>) => {
96+
this.onDragStart(ViewHandleType.Right)(event);
97+
};
98+
99+
onDragMove = (event: MouseEvent) => {
100+
if (
101+
!this.state.isDragging ||
102+
event.type !== 'mousemove' ||
103+
!this.hasInteractiveLayer()
104+
) {
105+
return;
106+
}
107+
108+
const rect = rectOfContent(this.props.interactiveLayerRef.current!);
109+
110+
// mouse x-coordinate relative to the interactive layer's left side
111+
const rawMouseX = (event.pageX - rect.x) / rect.width;
112+
113+
switch (this.state.currentDraggingHandle) {
114+
case ViewHandleType.Left: {
115+
const min = 0;
116+
const max = this.state.rightHandlePosition - MINIMUM_WINDOW_SIZE;
117+
118+
this.setState({
119+
// clamp mouseLeft to be within [0, rightHandlePosition - MINIMUM_WINDOW_SIZE]
120+
leftHandlePosition: clamp(rawMouseX, min, max),
121+
});
122+
break;
123+
}
124+
case ViewHandleType.Right: {
125+
const min = this.state.leftHandlePosition + MINIMUM_WINDOW_SIZE;
126+
const max = 1;
127+
128+
this.setState({
129+
// clamp mouseLeft to be within [leftHandlePosition + MINIMUM_WINDOW_SIZE, 1]
130+
rightHandlePosition: clamp(rawMouseX, min, max),
131+
});
132+
break;
133+
}
134+
default: {
135+
// this should never occur
136+
}
137+
}
138+
};
139+
140+
onDragEnd = (event: MouseEvent) => {
141+
if (
142+
!this.state.isDragging ||
143+
event.type !== 'mouseup' ||
144+
!this.hasInteractiveLayer()
145+
) {
146+
return;
147+
}
148+
149+
// remove listeners that were attached in onDragStart
150+
151+
window.removeEventListener('mousemove', this.onDragMove);
152+
window.removeEventListener('mouseup', this.onDragEnd);
153+
154+
// restore body styles
155+
156+
document.body.style.userSelect = this.previousUserSelect;
157+
this.previousUserSelect = null;
158+
159+
// indicate drag has ended
160+
161+
switch (this.state.currentDraggingHandle) {
162+
case ViewHandleType.Left: {
163+
this.setState(state => {
164+
return {
165+
isDragging: false,
166+
currentDraggingHandle: void 0,
167+
168+
// commit leftHandlePosition to be viewWindowStart
169+
viewWindowStart: state.leftHandlePosition,
170+
};
171+
});
172+
break;
173+
}
174+
case ViewHandleType.Right: {
175+
this.setState(state => {
176+
return {
177+
isDragging: false,
178+
currentDraggingHandle: void 0,
179+
180+
// commit rightHandlePosition to be viewWindowEnd
181+
viewWindowEnd: state.rightHandlePosition,
182+
};
183+
});
184+
break;
185+
}
186+
default: {
187+
// unreachable
188+
}
189+
}
190+
191+
this.setState({
192+
isDragging: false,
193+
currentDraggingHandle: void 0,
194+
});
195+
};
196+
197+
render() {
198+
const childrenProps = {
199+
isDragging: this.state.isDragging,
200+
201+
onLeftHandleDragStart: this.onLeftHandleDragStart,
202+
leftHandlePosition: this.state.leftHandlePosition,
203+
viewWindowStart: this.state.viewWindowStart,
204+
205+
onRightHandleDragStart: this.onRightHandleDragStart,
206+
rightHandlePosition: this.state.rightHandlePosition,
207+
viewWindowEnd: this.state.viewWindowEnd,
208+
};
209+
210+
return this.props.children(childrenProps);
211+
}
212+
}
213+
214+
export default DragManager;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import {t} from 'app/locale';
5+
import SentryTypes from 'app/sentryTypes';
6+
7+
import {Panel, PanelHeader, PanelBody} from 'app/components/panels';
8+
9+
import {SpanEntry, SentryEvent} from './types';
10+
import TransactionView from './transaction_view';
11+
12+
type SpansInterfacePropTypes = {
13+
event: SentryEvent;
14+
} & SpanEntry;
15+
16+
class SpansInterface extends React.Component<SpansInterfacePropTypes> {
17+
static propTypes = {
18+
event: SentryTypes.Event.isRequired,
19+
type: PropTypes.oneOf(['spans']).isRequired,
20+
data: PropTypes.arrayOf(
21+
PropTypes.shape({
22+
trace_id: PropTypes.string.isRequired,
23+
parent_span_id: PropTypes.string,
24+
span_id: PropTypes.string.isRequired,
25+
start_timestamp: PropTypes.number.isRequired,
26+
timestamp: PropTypes.number.isRequired, // same as end_timestamp
27+
same_process_as_parent: PropTypes.bool.isRequired,
28+
op: PropTypes.string.isRequired,
29+
data: PropTypes.object.isRequired,
30+
})
31+
).isRequired,
32+
};
33+
render() {
34+
const {event} = this.props;
35+
36+
return (
37+
<Panel>
38+
<PanelHeader disablePadding={false} hasButtons={false}>
39+
{t('Trace View')}
40+
</PanelHeader>
41+
<PanelBody>
42+
<TransactionView event={event} />
43+
</PanelBody>
44+
</Panel>
45+
);
46+
}
47+
}
48+
49+
export default SpansInterface;

0 commit comments

Comments
 (0)