-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(apm): Initial transactions view #13920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
66f054b
9e73332
7c3a9d6
2b8d698
b879451
b36da9c
bb662a9
31f8941
fff5913
86abd80
36e2976
21379b4
aa97eca
80289e5
c78ac7a
a70298d
2e44967
8d5ee44
4b56b11
de3f7c4
6f84d95
f24a662
ce5b6ee
9fa70c0
a0e6f5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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!); | ||
|
|
||
| // 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible this component unmounts before a dragend event happens?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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
currentwill is not going to be null/undefinedThere was a problem hiding this comment.
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 thatthis.props.interactiveLayerRef.currenthas 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
There was a problem hiding this comment.
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.