From b24825f618764f00c27280d1909cc1c9e63a1403 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Mon, 25 Sep 2017 17:56:51 -0400 Subject: [PATCH 01/20] WIP - Drag to change view-range on trace minimap --- .../TracePage/SpanGraph/MouseDraggedState.js | 89 +++++ .../TracePage/SpanGraph/Scrubber2.css | 39 +++ .../TracePage/SpanGraph/Scrubber2.js | 51 +++ .../TracePage/SpanGraph/SpanGraphV2.js | 101 ++++++ .../TracePage/SpanGraph/ViewingLayer.css | 67 ++++ .../TracePage/SpanGraph/ViewingLayer.js | 303 ++++++++++++++++++ .../TracePage/SpanGraph/render-into-canvas.js | 2 +- src/components/TracePage/index.js | 12 +- 8 files changed, 660 insertions(+), 4 deletions(-) create mode 100644 src/components/TracePage/SpanGraph/MouseDraggedState.js create mode 100644 src/components/TracePage/SpanGraph/Scrubber2.css create mode 100644 src/components/TracePage/SpanGraph/Scrubber2.js create mode 100644 src/components/TracePage/SpanGraph/SpanGraphV2.js create mode 100644 src/components/TracePage/SpanGraph/ViewingLayer.css create mode 100644 src/components/TracePage/SpanGraph/ViewingLayer.js diff --git a/src/components/TracePage/SpanGraph/MouseDraggedState.js b/src/components/TracePage/SpanGraph/MouseDraggedState.js new file mode 100644 index 0000000000..ac9393a44b --- /dev/null +++ b/src/components/TracePage/SpanGraph/MouseDraggedState.js @@ -0,0 +1,89 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import _clamp from 'lodash/clamp'; + +/** + * Which items of a {@link SpanDetail} component are expanded. + */ +export default class MouseDraggedState { + clientRect: ClientRect; + max: number; + min: number; + position: number; + start: number; + tag: string; + + static newFromOptions(options: { + clientRect: ClientRect, + clientX: number, + max?: number, + min?: number, + tag: string, + }) { + const opts = {}; + opts.clientRect = options.clientRect; + opts.max = options.max == null ? 1 : options.max; + opts.min = options.min == null ? 0 : options.min; + opts.position = (options.clientX - opts.clientRect.left) / (opts.clientRect.width || 1); + opts.start = opts.position; + opts.tag = options.tag; + return new MouseDraggedState(opts); + } + + constructor(options: { + clientRect: ClientRect, + max: number, + min: number, + position: number, + start: number, + tag: string, + }) { + Object.assign(this, options); + } + + newPositionFromClientX(clientX: number) { + const position = (clientX - this.clientRect.left) / (this.clientRect.width || 1); + const next = new MouseDraggedState(this); + next.position = _clamp(position, this.min, this.max); + return next; + } + + getLayout() { + if (this.position < this.start) { + return { + x: `${this.position * 100}%`, + // right: `${(1 - this.start) * 100}%`, + width: `${(this.start - this.position) * 100}%`, + // className: 'isDraggingLeft', + leadingX: `${this.position * 100}%`, + }; + } + return { + x: `${this.start * 100}%`, + // right: `${(1 - this.position) * 100}%`, + width: `${(this.position - this.start) * 100}%`, + // className: 'isDraggingRight', + leadingX: `${this.position * 100}%`, + }; + } +} diff --git a/src/components/TracePage/SpanGraph/Scrubber2.css b/src/components/TracePage/SpanGraph/Scrubber2.css new file mode 100644 index 0000000000..364cbfc47c --- /dev/null +++ b/src/components/TracePage/SpanGraph/Scrubber2.css @@ -0,0 +1,39 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +.Scrubber--handle { + cursor: ew-resize; + fill: #999; +} + +.Scrubber--handle:hover { + fill: #44f; +} + +.Scrubber--line { + pointer-events: none; + stroke: #999; +} + +.Scrubber--handle:hover + .Scrubber--line { + stroke: #44f; +} diff --git a/src/components/TracePage/SpanGraph/Scrubber2.js b/src/components/TracePage/SpanGraph/Scrubber2.js new file mode 100644 index 0000000000..8216fdb128 --- /dev/null +++ b/src/components/TracePage/SpanGraph/Scrubber2.js @@ -0,0 +1,51 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import React from 'react'; + +import './Scrubber2.css'; + +type ScrubberProps = { + position: number, + onMouseDown: (SyntheticMouseEvent) => void, + onMouseEnter: (SyntheticMouseEvent) => void, + onMouseLeave: (SyntheticMouseEvent) => void, +}; + +export default function Scrubber({ position, onMouseDown, onMouseEnter, onMouseLeave }: ScrubberProps) { + const xPercent = `${position * 100}%`; + return ( + + + + + ); +} diff --git a/src/components/TracePage/SpanGraph/SpanGraphV2.js b/src/components/TracePage/SpanGraph/SpanGraphV2.js new file mode 100644 index 0000000000..011e33d2f7 --- /dev/null +++ b/src/components/TracePage/SpanGraph/SpanGraphV2.js @@ -0,0 +1,101 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { window } from 'global'; + +import CanvasSpanGraph from './CanvasSpanGraph'; +import TickLabels from './TickLabels'; +import ViewingLayer from './ViewingLayer'; +import type { Trace } from '../../../types'; + +import './index.css'; + +const TIMELINE_TICK_INTERVAL = 4; + +type SpanGraphProps = { + height: number, + trace: Trace, + viewRange: [number, number], + updateViewRange: ([number, number]) => void, +}; + +export default class SpanGraph extends React.Component { + props: SpanGraphProps; + + static defaultProps = { + height: 60, + }; + + constructor(props: SpanGraphProps) { + super(props); + } + + // shouldComponentUpdate(nextProps: SpanGraphProps, nextState: SpanGraphState) { + // const { trace: newTrace, viewRange: newViewRange } = nextProps; + // const { + // currentlyDragging: newCurrentlyDragging, + // leftBound: newLeftBound, + // rightBound: newRightBound, + // } = nextState; + // const { trace, viewRange } = this.props; + // const { currentlyDragging, leftBound, rightBound } = this.state; + + // return ( + // trace.traceID !== newTrace.traceID || + // viewRange[0] !== newViewRange[0] || + // viewRange[1] !== newViewRange[1] || + // currentlyDragging !== newCurrentlyDragging || + // leftBound !== newLeftBound || + // rightBound !== newRightBound + // ); + // } + + render() { + const { height, trace, viewRange, updateViewRange } = this.props; + if (!trace) { + return
; + } + return ( +
+ +
+ ({ + valueOffset: span.relativeStartTime, + valueWidth: span.duration, + serviceName: span.process.serviceName, + }))} + /> + +
+
+ ); + } +} diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.css b/src/components/TracePage/SpanGraph/ViewingLayer.css new file mode 100644 index 0000000000..ef49976c0d --- /dev/null +++ b/src/components/TracePage/SpanGraph/ViewingLayer.css @@ -0,0 +1,67 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +.ViewingLayer { + position: relative; + z-index: 1; +} + +.ViewingLayer--graph { + border: 1px solid #999; + /* need !important here to overcome something from semantic UI */ + overflow: visible !important; + position: relative; + transform-origin: 0 0; + width: 100%; +} + +.ViewingLayer--inactive { + fill: rgba(214, 214, 214, 0.5); +} + +.ViewingLayer--cursorGuide { + stroke: #f44; + stroke-width: 2.3; +} + +.ViewingLayer--draggedShift { + fill-opacity: 0.3; +} + +.ViewingLayer--draggedShift.isScrubberDrag, +.ViewingLayer--draggedEdge.isScrubberDrag { + fill: #44f; +} + +.ViewingLayer--draggedShift.isResetDrag, +.ViewingLayer--draggedEdge.isResetDrag { + fill: #f44; +} + +.ViewingLayer--fullOverlay { + bottom: 0; + cursor: ew-resize; + left: 0; + position: fixed; + right: 0; + top: 0; +} \ No newline at end of file diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.js b/src/components/TracePage/SpanGraph/ViewingLayer.js new file mode 100644 index 0000000000..a3fe338adc --- /dev/null +++ b/src/components/TracePage/SpanGraph/ViewingLayer.js @@ -0,0 +1,303 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import * as React from 'react'; +import cx from 'classnames'; + +import MouseDraggedState from './MouseDraggedState'; +import GraphTicks from './GraphTicks'; +import Scrubber2 from './Scrubber2'; + +import './ViewingLayer.css'; + +type ViewingLayerProps = { + height: number, + numTicks: number, + updateViewRange: ([number, number]) => void, + viewRange: [number, number], +}; + +type ViewingLayerState = { + cursorX: ?number, + draggedState: ?MouseDraggedState, + preventCursorLine: boolean, +}; + +const LEFT_MOUSE_BUTTON = 0; + +const dragTags = { + RESET: 'RESET', + SCRUB_INTERMEDIATE_STATE: 'SCRUBBER_INTERMEDIATE_STATE', + SCRUB_LEFT_HANDLE: 'SCRUB_LEFT_HANDLE', + SCRUB_RIGHT_HANDLE: 'SCRUB_RIGHT_HANDLE', +}; + +export default class ViewingLayer extends React.PureComponent { + props: ViewingLayerProps; + state: ViewingLayerState; + + _root: ?Element; + _rootClientRect: ?ClientRect; + _windowListenersAttached: boolean; + + constructor(props: ViewingLayerProps) { + super(props); + this.state = { + cursorX: undefined, + draggedState: undefined, + preventCursorLine: false, + }; + this._root = undefined; + this._rootClientRect = undefined; + this._windowListenersAttached = false; + this._setRoot = this._setRoot.bind(this); + this._handleScrubberMouseEnter = this._handleScrubberMouseEnter.bind(this); + this._handleScrubberMouseLeave = this._handleScrubberMouseLeave.bind(this); + this._handleScrubberMouseDown = this._handleScrubberMouseDown.bind(this); + this._handleRootMouseMove = this._handleRootMouseMove.bind(this); + this._handleRootMouseLeave = this._handleRootMouseLeave.bind(this); + this._handleRootMouseDown = this._handleRootMouseDown.bind(this); + this._handleWindowMouseMove = this._handleWindowMouseMove.bind(this); + this._handleWindowMouseUp = this._handleWindowMouseUp.bind(this); + } + + _setRoot = function _setRoot(elm: ?Element) { + this._root = elm; + if (elm) { + this._rootClientRect = elm.getBoundingClientRect(); + } else { + this._rootClientRect = undefined; + } + }; + + _handleScrubberMouseEnter = function _handleScrubberMouseEnter() { + this.setState({ ...this.state, preventCursorLine: true }); + }; + + _handleScrubberMouseLeave = function _handleScrubberMouseLeave() { + this.setState({ ...this.state, preventCursorLine: false }); + }; + + _handleScrubberMouseDown = function _handleScrubberMouseDown(event: SyntheticMouseEvent) { + console.log('scrubber mouse down', event); + const { button, clientX } = event; + if (!this._root || button !== LEFT_MOUSE_BUTTON) { + return; + } + event.stopPropagation(); + + // the ClientRect retrieved when the SVG is initially rendered has an + // inaccurate width, so refresh the ClientRect on mouse down + this._rootClientRect = this._root.getBoundingClientRect(); + window.addEventListener('mousemove', this._handleWindowMouseMove); + window.addEventListener('mouseup', this._handleWindowMouseUp); + this._windowListenersAttached = true; + const draggedState = MouseDraggedState.newFromOptions({ + clientX, + clientRect: this._rootClientRect, + max: 1, + min: 0, + tag: dragTags.SCRUB_INTERMEDIATE_STATE, + }); + const position = draggedState.position; + const [leftViewPosition, rightViewPosition] = this.props.viewRange; + if (Math.abs(leftViewPosition - position) < Math.abs(rightViewPosition - position)) { + draggedState.tag = dragTags.SCRUB_LEFT_HANDLE; + draggedState.max = rightViewPosition; + draggedState.start = leftViewPosition; + } else { + draggedState.tag = dragTags.SCRUB_RIGHT_HANDLE; + draggedState.min = leftViewPosition; + draggedState.start = rightViewPosition; + } + this.setState({ ...this.state, draggedState }); + }; + + _handleRootMouseMove = function _handleRootMouseMove({ clientX }: SyntheticMouseEvent) { + if (this._rootClientRect) { + this.setState({ ...this.state, cursorX: clientX - this._rootClientRect.left }); + } + }; + + _handleRootMouseLeave = function _handleRootMouseLeave() { + this.setState({ ...this.state, cursorX: undefined }); + }; + + _handleRootMouseDown = function _handleRootMouseDown({ button, clientX }: SyntheticMouseEvent) { + if (!this._root || button !== LEFT_MOUSE_BUTTON) { + return; + } + // the ClientRect retrieved when the SVG is initially rendered has an + // inaccurate width, so refresh the ClientRect on mouse down + this._rootClientRect = this._root.getBoundingClientRect(); + window.addEventListener('mousemove', this._handleWindowMouseMove); + window.addEventListener('mouseup', this._handleWindowMouseUp); + this._windowListenersAttached = true; + const draggedState = MouseDraggedState.newFromOptions({ + clientX, + clientRect: this._rootClientRect, + max: 1, + min: 0, + tag: dragTags.RESET, + }); + this.setState({ ...this.state, draggedState }); + }; + + _handleWindowMouseMove = function _handleWindowMouseMove({ clientX }: SyntheticMouseEvent) { + if (this.state.draggedState) { + const draggedState = this.state.draggedState.newPositionFromClientX(clientX); + this.setState({ ...this.state, draggedState }); + } + }; + + _handleWindowMouseUp = function _handleWindowMouseUp({ clientX }: SyntheticMouseEvent) { + window.removeEventListener('mousemove', this._handleWindowMouseMove); + window.removeEventListener('mouseup', this._handleWindowMouseUp); + this._windowListenersAttached = false; + if (this.state.draggedState) { + const draggedState = this.state.draggedState.newPositionFromClientX(clientX); + const { start: draggedFrom, position: draggedTo } = draggedState; + let viewStart; + let viewEnd; + if (draggedState.tag === dragTags.RESET) { + if (draggedFrom < draggedTo) { + viewStart = draggedFrom; + viewEnd = draggedTo; + } else { + viewStart = draggedTo; + viewEnd = draggedFrom; + } + } else if (draggedState.tag === dragTags.SCRUB_LEFT_HANDLE) { + const [_, currentViewEnd] = this.props.viewRange; + viewStart = draggedTo; + viewEnd = currentViewEnd; + } else { + const [currentViewStart, _] = this.props.viewRange; + viewStart = currentViewStart; + viewEnd = draggedTo; + } + this.props.updateViewRange(viewStart, viewEnd); + // reset cursorX to prevent a remnant cursorX from missing the mouseleave + // event + this.setState({ ...this.state, cursorX: undefined, draggedState: undefined, preventCursorLine: false }); + } + }; + + render() { + const { height, viewRange, numTicks } = this.props; + const { cursorX, draggedState, preventCursorLine } = this.state; + const [leftBound, rightBound] = viewRange; + let leftInactive = 0; + if (leftBound) { + leftInactive = leftBound * 100; + } + let rightInactive = 100; + if (rightBound) { + rightInactive = 100 - rightBound * 100; + } + let isScrubberDrag = false; + let dragMarkers: ?(React.Node[]); + let scrubDragMarkers: ?(React.Node[]); + let fullOverlay: ?React.Node; + let cursorGuide: ?React.Node; + // if (draggedState && draggedState.tag === dragTags.RESET) { + if (draggedState) { + const { tag } = draggedState; + isScrubberDrag = tag !== dragTags.RESET; + const cls = cx({ + isScrubberDrag, + isResetDrag: !isScrubberDrag, + }); + const layout = draggedState.getLayout(); + dragMarkers = [ + , + , + ]; + fullOverlay =
; + } else if (cursorX != null && !preventCursorLine) { + cursorGuide = ( + + ); + } + return ( +
+ + {leftInactive > 0 && + } + {rightInactive < 100 && + } + + {cursorGuide} + {isScrubberDrag && dragMarkers} + + + {!isScrubberDrag && dragMarkers} + + {fullOverlay} +
+ ); + } +} diff --git a/src/components/TracePage/SpanGraph/render-into-canvas.js b/src/components/TracePage/SpanGraph/render-into-canvas.js index eb53e9c998..d1ea53b0f8 100644 --- a/src/components/TracePage/SpanGraph/render-into-canvas.js +++ b/src/components/TracePage/SpanGraph/render-into-canvas.js @@ -21,7 +21,7 @@ // THE SOFTWARE. const CV_WIDTH = 4000; -const MIN_WIDTH = 50; +const MIN_WIDTH = 16; const MIN_TOTAL_HEIGHT = 60; export default function renderIntoCanvas( diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index 680933963e..0d91951420 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -26,7 +26,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import TracePageHeader from './TracePageHeader'; -import SpanGraph from './SpanGraph'; +import SpanGraph from './SpanGraph/SpanGraphV2'; import TraceTimelineViewer from './TraceTimelineViewer'; import NotFound from '../App/NotFound'; import * as jaegerApiActions from '../../actions/jaeger-api'; @@ -65,6 +65,7 @@ export default class TracePage extends Component { this.headerElm = null; this.setHeaderHeight = this.setHeaderHeight.bind(this); this.toggleSlimView = this.toggleSlimView.bind(this); + this.updateTimeRangeFilter = this.updateTimeRangeFilter.bind(this); } getChildContext() { @@ -73,7 +74,7 @@ export default class TracePage extends Component { delete state.timeRangeFilter; return { updateTextFilter: this.updateTextFilter.bind(this), - updateTimeRangeFilter: this.updateTimeRangeFilter.bind(this), + updateTimeRangeFilter: this.updateTimeRangeFilter, ...state, }; } @@ -172,7 +173,12 @@ export default class TracePage extends Component { traceID={traceID} onSlimViewClicked={this.toggleSlimView} /> - {!slimView && } + {!slimView && + } {headerHeight &&
From f574cd0b0633a5779bc189f272c8202938f374f0 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Mon, 25 Sep 2017 19:36:49 -0400 Subject: [PATCH 02/20] WIP - Drag to change view-range on trace minimap --- .../TracePage/SpanGraph/MouseDraggedState.js | 10 +- .../TracePage/SpanGraph/Scrubber.css | 27 +-- .../TracePage/SpanGraph/Scrubber.js | 50 +--- .../TracePage/SpanGraph/Scrubber2.css | 39 --- .../TracePage/SpanGraph/Scrubber2.js | 51 ---- .../TracePage/SpanGraph/SpanGraphV2.js | 101 -------- .../TracePage/SpanGraph/ViewingLayer.js | 64 ++--- src/components/TracePage/SpanGraph/index.css | 43 ---- src/components/TracePage/SpanGraph/index.js | 224 +++--------------- src/components/TracePage/index.js | 2 +- 10 files changed, 86 insertions(+), 525 deletions(-) delete mode 100644 src/components/TracePage/SpanGraph/Scrubber2.css delete mode 100644 src/components/TracePage/SpanGraph/Scrubber2.js delete mode 100644 src/components/TracePage/SpanGraph/SpanGraphV2.js delete mode 100644 src/components/TracePage/SpanGraph/index.css diff --git a/src/components/TracePage/SpanGraph/MouseDraggedState.js b/src/components/TracePage/SpanGraph/MouseDraggedState.js index ac9393a44b..c8e36de5a4 100644 --- a/src/components/TracePage/SpanGraph/MouseDraggedState.js +++ b/src/components/TracePage/SpanGraph/MouseDraggedState.js @@ -22,9 +22,6 @@ import _clamp from 'lodash/clamp'; -/** - * Which items of a {@link SpanDetail} component are expanded. - */ export default class MouseDraggedState { clientRect: ClientRect; max: number; @@ -38,6 +35,7 @@ export default class MouseDraggedState { clientX: number, max?: number, min?: number, + start?: number, tag: string, }) { const opts = {}; @@ -45,7 +43,7 @@ export default class MouseDraggedState { opts.max = options.max == null ? 1 : options.max; opts.min = options.min == null ? 0 : options.min; opts.position = (options.clientX - opts.clientRect.left) / (opts.clientRect.width || 1); - opts.start = opts.position; + opts.start = options.start == null ? opts.position : options.start; opts.tag = options.tag; return new MouseDraggedState(opts); } @@ -72,17 +70,13 @@ export default class MouseDraggedState { if (this.position < this.start) { return { x: `${this.position * 100}%`, - // right: `${(1 - this.start) * 100}%`, width: `${(this.start - this.position) * 100}%`, - // className: 'isDraggingLeft', leadingX: `${this.position * 100}%`, }; } return { x: `${this.start * 100}%`, - // right: `${(1 - this.position) * 100}%`, width: `${(this.position - this.start) * 100}%`, - // className: 'isDraggingRight', leadingX: `${this.position * 100}%`, }; } diff --git a/src/components/TracePage/SpanGraph/Scrubber.css b/src/components/TracePage/SpanGraph/Scrubber.css index accdda7bbd..364cbfc47c 100644 --- a/src/components/TracePage/SpanGraph/Scrubber.css +++ b/src/components/TracePage/SpanGraph/Scrubber.css @@ -20,31 +20,20 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -.timeline-scrubber { +.Scrubber--handle { cursor: ew-resize; + fill: #999; } -.timeline-scrubber__line { - stroke: #999; - stroke-width: 1; -} - -.timeline-scrubber:hover .timeline-scrubber__line { - stroke: #777; +.Scrubber--handle:hover { + fill: #44f; } -.timeline-scrubber__handle { +.Scrubber--line { + pointer-events: none; stroke: #999; - fill: #fff; } -.timeline-scrubber:hover .timeline-scrubber__handle { - stroke: #777; -} - -.timeline-scrubber__handle--grip { - fill: #bbb; -} -.timeline-scrubber:hover .timeline-scrubber__handle--grip { - fill: #999; +.Scrubber--handle:hover + .Scrubber--line { + stroke: #44f; } diff --git a/src/components/TracePage/SpanGraph/Scrubber.js b/src/components/TracePage/SpanGraph/Scrubber.js index 16f2008206..029868065d 100644 --- a/src/components/TracePage/SpanGraph/Scrubber.js +++ b/src/components/TracePage/SpanGraph/Scrubber.js @@ -27,51 +27,25 @@ import './Scrubber.css'; type ScrubberProps = { position: number, onMouseDown: (SyntheticMouseEvent) => void, - handleTopOffset: number, - handleWidth: number, - handleHeight: number, + onMouseEnter: (SyntheticMouseEvent) => void, + onMouseLeave: (SyntheticMouseEvent) => void, }; -const HANDLE_WIDTH = 6; -const HANDLE_HEIGHT = 20; -const HANDLE_TOP_OFFSET = 0; - -export default function Scrubber({ - position, - onMouseDown, - handleTopOffset = HANDLE_TOP_OFFSET, - handleWidth = HANDLE_WIDTH, - handleHeight = HANDLE_HEIGHT, -}: ScrubberProps) { +export default function Scrubber({ position, onMouseDown, onMouseEnter, onMouseLeave }: ScrubberProps) { const xPercent = `${position * 100}%`; return ( - - + - - - + ); } diff --git a/src/components/TracePage/SpanGraph/Scrubber2.css b/src/components/TracePage/SpanGraph/Scrubber2.css deleted file mode 100644 index 364cbfc47c..0000000000 --- a/src/components/TracePage/SpanGraph/Scrubber2.css +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright (c) 2017 Uber Technologies, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -.Scrubber--handle { - cursor: ew-resize; - fill: #999; -} - -.Scrubber--handle:hover { - fill: #44f; -} - -.Scrubber--line { - pointer-events: none; - stroke: #999; -} - -.Scrubber--handle:hover + .Scrubber--line { - stroke: #44f; -} diff --git a/src/components/TracePage/SpanGraph/Scrubber2.js b/src/components/TracePage/SpanGraph/Scrubber2.js deleted file mode 100644 index 8216fdb128..0000000000 --- a/src/components/TracePage/SpanGraph/Scrubber2.js +++ /dev/null @@ -1,51 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import React from 'react'; - -import './Scrubber2.css'; - -type ScrubberProps = { - position: number, - onMouseDown: (SyntheticMouseEvent) => void, - onMouseEnter: (SyntheticMouseEvent) => void, - onMouseLeave: (SyntheticMouseEvent) => void, -}; - -export default function Scrubber({ position, onMouseDown, onMouseEnter, onMouseLeave }: ScrubberProps) { - const xPercent = `${position * 100}%`; - return ( - - - - - ); -} diff --git a/src/components/TracePage/SpanGraph/SpanGraphV2.js b/src/components/TracePage/SpanGraph/SpanGraphV2.js deleted file mode 100644 index 011e33d2f7..0000000000 --- a/src/components/TracePage/SpanGraph/SpanGraphV2.js +++ /dev/null @@ -1,101 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { window } from 'global'; - -import CanvasSpanGraph from './CanvasSpanGraph'; -import TickLabels from './TickLabels'; -import ViewingLayer from './ViewingLayer'; -import type { Trace } from '../../../types'; - -import './index.css'; - -const TIMELINE_TICK_INTERVAL = 4; - -type SpanGraphProps = { - height: number, - trace: Trace, - viewRange: [number, number], - updateViewRange: ([number, number]) => void, -}; - -export default class SpanGraph extends React.Component { - props: SpanGraphProps; - - static defaultProps = { - height: 60, - }; - - constructor(props: SpanGraphProps) { - super(props); - } - - // shouldComponentUpdate(nextProps: SpanGraphProps, nextState: SpanGraphState) { - // const { trace: newTrace, viewRange: newViewRange } = nextProps; - // const { - // currentlyDragging: newCurrentlyDragging, - // leftBound: newLeftBound, - // rightBound: newRightBound, - // } = nextState; - // const { trace, viewRange } = this.props; - // const { currentlyDragging, leftBound, rightBound } = this.state; - - // return ( - // trace.traceID !== newTrace.traceID || - // viewRange[0] !== newViewRange[0] || - // viewRange[1] !== newViewRange[1] || - // currentlyDragging !== newCurrentlyDragging || - // leftBound !== newLeftBound || - // rightBound !== newRightBound - // ); - // } - - render() { - const { height, trace, viewRange, updateViewRange } = this.props; - if (!trace) { - return
; - } - return ( -
- -
- ({ - valueOffset: span.relativeStartTime, - valueWidth: span.duration, - serviceName: span.process.serviceName, - }))} - /> - -
-
- ); - } -} diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.js b/src/components/TracePage/SpanGraph/ViewingLayer.js index a3fe338adc..6f612be520 100644 --- a/src/components/TracePage/SpanGraph/ViewingLayer.js +++ b/src/components/TracePage/SpanGraph/ViewingLayer.js @@ -25,7 +25,7 @@ import cx from 'classnames'; import MouseDraggedState from './MouseDraggedState'; import GraphTicks from './GraphTicks'; -import Scrubber2 from './Scrubber2'; +import Scrubber from './Scrubber'; import './ViewingLayer.css'; @@ -58,6 +58,8 @@ export default class ViewingLayer extends React.PureComponent) => void; + _handleRightScrubberMouseDown: (SyntheticMouseEvent) => void; constructor(props: ViewingLayerProps) { super(props); @@ -69,10 +71,16 @@ export default class ViewingLayer extends React.PureComponent) => { + this._handleScrubberMouseDown(dragTags.SCRUB_LEFT_HANDLE, event); + }; + this._handleRightScrubberMouseDown = (event: SyntheticMouseEvent) => { + this._handleScrubberMouseDown(dragTags.SCRUB_RIGHT_HANDLE, event); + }; this._handleRootMouseMove = this._handleRootMouseMove.bind(this); this._handleRootMouseLeave = this._handleRootMouseLeave.bind(this); this._handleRootMouseDown = this._handleRootMouseDown.bind(this); @@ -97,40 +105,40 @@ export default class ViewingLayer extends React.PureComponent) { - console.log('scrubber mouse down', event); + _handleScrubberMouseDown(tag: string, event: SyntheticMouseEvent) { const { button, clientX } = event; - if (!this._root || button !== LEFT_MOUSE_BUTTON) { + if (button !== LEFT_MOUSE_BUTTON) { return; } + // stop propagation so the root mousedown listener does not hi-jack the show event.stopPropagation(); - + if (!this._root) { + return; + } // the ClientRect retrieved when the SVG is initially rendered has an // inaccurate width, so refresh the ClientRect on mouse down this._rootClientRect = this._root.getBoundingClientRect(); window.addEventListener('mousemove', this._handleWindowMouseMove); window.addEventListener('mouseup', this._handleWindowMouseUp); this._windowListenersAttached = true; - const draggedState = MouseDraggedState.newFromOptions({ + const [leftViewPosition, rightViewPosition] = this.props.viewRange; + const opts: Object = { clientX, + tag, clientRect: this._rootClientRect, - max: 1, - min: 0, - tag: dragTags.SCRUB_INTERMEDIATE_STATE, - }); - const position = draggedState.position; - const [leftViewPosition, rightViewPosition] = this.props.viewRange; - if (Math.abs(leftViewPosition - position) < Math.abs(rightViewPosition - position)) { - draggedState.tag = dragTags.SCRUB_LEFT_HANDLE; - draggedState.max = rightViewPosition; - draggedState.start = leftViewPosition; + }; + if (tag === dragTags.SCRUB_LEFT_HANDLE) { + opts.start = leftViewPosition; + opts.min = 0; + opts.max = rightViewPosition; } else { - draggedState.tag = dragTags.SCRUB_RIGHT_HANDLE; - draggedState.min = leftViewPosition; - draggedState.start = rightViewPosition; + opts.start = rightViewPosition; + opts.min = leftViewPosition; + opts.max = 1; } + const draggedState = MouseDraggedState.newFromOptions(opts); this.setState({ ...this.state, draggedState }); - }; + } _handleRootMouseMove = function _handleRootMouseMove({ clientX }: SyntheticMouseEvent) { if (this._rootClientRect) { @@ -187,11 +195,11 @@ export default class ViewingLayer extends React.PureComponent {cursorGuide} {isScrubberDrag && dragMarkers} - - diff --git a/src/components/TracePage/SpanGraph/index.css b/src/components/TracePage/SpanGraph/index.css deleted file mode 100644 index 77a28a37b9..0000000000 --- a/src/components/TracePage/SpanGraph/index.css +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright (c) 2017 Uber Technologies, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -.SpanGraph--zlayer { - position: relative; - z-index: 1; -} - -.SpanGraph--graph { - border: 1px solid #999; - /* need !important here to overcome something from semantic UI */ - overflow: visible !important; - position: relative; - transform-origin: 0 0; - width: 100%; -} - -.SpanGraph--graph.is-dragging { - cursor: ew-resize; -} - -.SpanGraph--inactive { - fill: rgba(214, 214, 214, 0.5); -} \ No newline at end of file diff --git a/src/components/TracePage/SpanGraph/index.js b/src/components/TracePage/SpanGraph/index.js index cae22b935f..68c9cb9a5e 100644 --- a/src/components/TracePage/SpanGraph/index.js +++ b/src/components/TracePage/SpanGraph/index.js @@ -21,216 +21,48 @@ // THE SOFTWARE. import * as React from 'react'; -import PropTypes from 'prop-types'; -import { window } from 'global'; -import GraphTicks from './GraphTicks'; import CanvasSpanGraph from './CanvasSpanGraph'; import TickLabels from './TickLabels'; -import Scrubber from './Scrubber'; +import ViewingLayer from './ViewingLayer'; import type { Trace } from '../../../types'; -import './index.css'; - const TIMELINE_TICK_INTERVAL = 4; type SpanGraphProps = { height: number, trace: Trace, viewRange: [number, number], + updateViewRange: ([number, number]) => void, }; -type SpanGraphState = { - currentlyDragging: ?string, - leftBound: ?number, - prevX: ?number, - rightBound: ?number, -}; - -export default class SpanGraph extends React.Component { - props: SpanGraphProps; - state: SpanGraphState; - - _wrapper: ?HTMLElement; - _publishIntervalID: ?number; - - static defaultProps = { - height: 60, - }; - - static contextTypes = { - updateTimeRangeFilter: PropTypes.func.isRequired, - }; +export default function SpanGraph(props: SpanGraphProps) { + const { height, trace, viewRange, updateViewRange } = props; - constructor(props: SpanGraphProps) { - super(props); - this.state = { - currentlyDragging: null, - leftBound: null, - prevX: null, - rightBound: null, - }; - this._wrapper = undefined; - this._setWrapper = this._setWrapper.bind(this); - this._publishTimeRange = this._publishTimeRange.bind(this); - this._publishIntervalID = undefined; + if (!trace) { + return
; } - - shouldComponentUpdate(nextProps: SpanGraphProps, nextState: SpanGraphState) { - const { trace: newTrace, viewRange: newViewRange } = nextProps; - const { - currentlyDragging: newCurrentlyDragging, - leftBound: newLeftBound, - rightBound: newRightBound, - } = nextState; - const { trace, viewRange } = this.props; - const { currentlyDragging, leftBound, rightBound } = this.state; - - return ( - trace.traceID !== newTrace.traceID || - viewRange[0] !== newViewRange[0] || - viewRange[1] !== newViewRange[1] || - currentlyDragging !== newCurrentlyDragging || - leftBound !== newLeftBound || - rightBound !== newRightBound - ); - } - - _setWrapper = function _setWrapper(elm: React.Node) { - this._wrapper = elm; - }; - - _startDragging(boundName: string, { clientX }: SyntheticMouseEvent) { - const { viewRange } = this.props; - const [leftBound, rightBound] = viewRange; - - this.setState({ currentlyDragging: boundName, prevX: clientX, leftBound, rightBound }); - - const mouseMoveHandler = (...args) => this._onMouseMove(...args); - const mouseUpHandler = () => { - this._stopDragging(); - window.removeEventListener('mouseup', mouseUpHandler); - window.removeEventListener('mousemove', mouseMoveHandler); - }; - - window.addEventListener('mouseup', mouseUpHandler); - window.addEventListener('mousemove', mouseMoveHandler); - } - - _stopDragging() { - this._publishTimeRange(); - this.setState({ currentlyDragging: null, prevX: null }); - } - - _publishTimeRange = function _publishTimeRange() { - const { currentlyDragging, leftBound, rightBound } = this.state; - const { updateTimeRangeFilter } = this.context; - clearTimeout(this._publishIntervalID); - this._publishIntervalID = undefined; - if (currentlyDragging) { - updateTimeRangeFilter(leftBound, rightBound); - } - }; - - _onMouseMove({ clientX }: SyntheticMouseEvent) { - const { currentlyDragging } = this.state; - let { leftBound, rightBound } = this.state; - if (!currentlyDragging || !this._wrapper) { - return; - } - const newValue = clientX / this._wrapper.clientWidth; - switch (currentlyDragging) { - case 'leftBound': - leftBound = Math.max(0, newValue); - break; - case 'rightBound': - rightBound = Math.min(1, newValue); - break; - default: - break; - } - this.setState({ prevX: clientX, leftBound, rightBound }); - if (this._publishIntervalID == null) { - this._publishIntervalID = window.requestAnimationFrame(this._publishTimeRange); - } - } - - render() { - const { height, trace, viewRange } = this.props; - if (!trace) { - return
; - } - const { currentlyDragging } = this.state; - let { leftBound, rightBound } = this.state; - if (!currentlyDragging) { - leftBound = viewRange[0]; - rightBound = viewRange[1]; - } - let leftInactive; - if (leftBound) { - leftInactive = leftBound * 100; - } - let rightInactive; - if (rightBound) { - rightInactive = 100 - rightBound * 100; - } - return ( -
- -
- ({ - valueOffset: span.relativeStartTime, - valueWidth: span.duration, - serviceName: span.process.serviceName, - }))} - /> -
- - {leftInactive && - } - {rightInactive && - } - ({ - valueOffset: span.relativeStartTime, - valueWidth: span.duration, - serviceName: span.process.serviceName, - }))} - /> - { - this._startDragging('leftBound', event)} - /> - } - { - this._startDragging('rightBound', event)} - /> - } - -
-
+ return ( +
+ +
+ ({ + valueOffset: span.relativeStartTime, + valueWidth: span.duration, + serviceName: span.process.serviceName, + }))} + /> +
- ); - } +
+ ); } + +SpanGraph.defaultProps = { height: 60 }; diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index 0d91951420..28f8d47d3a 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -26,7 +26,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import TracePageHeader from './TracePageHeader'; -import SpanGraph from './SpanGraph/SpanGraphV2'; +import SpanGraph from './SpanGraph'; import TraceTimelineViewer from './TraceTimelineViewer'; import NotFound from '../App/NotFound'; import * as jaegerApiActions from '../../actions/jaeger-api'; From aef4225b8da2c92598f4d307a20c527737107ac7 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Tue, 26 Sep 2017 14:14:14 -0400 Subject: [PATCH 03/20] Small adjustments to visuals of minimap UX --- .../TracePage/SpanGraph/Scrubber.css | 6 +++-- .../TracePage/SpanGraph/Scrubber.js | 17 ++++++++++--- .../TracePage/SpanGraph/ViewingLayer.css | 7 +++--- .../TracePage/SpanGraph/ViewingLayer.js | 25 +++++++++++++------ 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/components/TracePage/SpanGraph/Scrubber.css b/src/components/TracePage/SpanGraph/Scrubber.css index 364cbfc47c..273cf37aa4 100644 --- a/src/components/TracePage/SpanGraph/Scrubber.css +++ b/src/components/TracePage/SpanGraph/Scrubber.css @@ -22,18 +22,20 @@ THE SOFTWARE. .Scrubber--handle { cursor: ew-resize; - fill: #999; + fill: #555; } +.Scrubber.isDragging > .Scrubber--handle, .Scrubber--handle:hover { fill: #44f; } .Scrubber--line { pointer-events: none; - stroke: #999; + stroke: #555; } +.Scrubber.isDragging > .Scrubber--line, .Scrubber--handle:hover + .Scrubber--line { stroke: #44f; } diff --git a/src/components/TracePage/SpanGraph/Scrubber.js b/src/components/TracePage/SpanGraph/Scrubber.js index 029868065d..b1e942029a 100644 --- a/src/components/TracePage/SpanGraph/Scrubber.js +++ b/src/components/TracePage/SpanGraph/Scrubber.js @@ -21,25 +21,34 @@ // THE SOFTWARE. import React from 'react'; +import cx from 'classnames'; import './Scrubber.css'; type ScrubberProps = { + isDragging: boolean, position: number, onMouseDown: (SyntheticMouseEvent) => void, onMouseEnter: (SyntheticMouseEvent) => void, onMouseLeave: (SyntheticMouseEvent) => void, }; -export default function Scrubber({ position, onMouseDown, onMouseEnter, onMouseLeave }: ScrubberProps) { +export default function Scrubber({ + isDragging, + onMouseDown, + onMouseEnter, + onMouseLeave, + position, +}: ScrubberProps) { const xPercent = `${position * 100}%`; + const className = cx('Scrubber', { isDragging }); return ( - + , ]; @@ -261,14 +261,23 @@ export default class ViewingLayer extends React.PureComponent ); } return ( -
+
Date: Wed, 27 Sep 2017 14:06:38 -0400 Subject: [PATCH 04/20] Refactor trace minimap mouse UX TODO: fix tests. Primary reason for this refactor is to lay the ground-work for keyboard shortcuts. - Got rid of components/TracePage#context - components/TracePage#state.viewRange changed to have `.current` and `.next` with `.next` being the "unapplied" change from the mouse UX - Minimap UX now publishes unapplied changes (user dragging on minimap) to `viewRange.next` instead of retaining them in the minimap state - Minimap rendering changed to draw highlights based on `viewRange.next` regardless of UX state --- package.json | 2 +- .../TracePage/SpanGraph/Scrubber.css | 2 +- .../TracePage/SpanGraph/ViewingLayer.css | 12 +- .../TracePage/SpanGraph/ViewingLayer.js | 190 +++++++++--------- src/components/TracePage/SpanGraph/index.js | 9 +- src/components/TracePage/TracePageHeader.js | 20 +- .../TraceTimelineViewer/TimelineHeaderRow.css | 2 +- .../TraceTimelineViewer/TimelineHeaderRow.js | 2 +- .../TracePage/TraceTimelineViewer/index.js | 8 +- src/components/TracePage/index.js | 125 ++++++------ src/components/TracePage/types.js | 34 ++++ 11 files changed, 218 insertions(+), 188 deletions(-) create mode 100644 src/components/TracePage/types.js diff --git a/package.json b/package.json index e17e689989..b6e60066e1 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "clear-homepage": "json -I -f package.json -e 'delete this.homepage'", "deploy-docs": "./bin/deploy-docs.sh", "postpublish": "npm run build:docs && npm run deploy-docs", - "add-license": "uber-licence", + "add-license": "uber-licence --dir src", "precommit": "lint-staged" }, "lint-staged": { diff --git a/src/components/TracePage/SpanGraph/Scrubber.css b/src/components/TracePage/SpanGraph/Scrubber.css index 273cf37aa4..d9f9d61ae9 100644 --- a/src/components/TracePage/SpanGraph/Scrubber.css +++ b/src/components/TracePage/SpanGraph/Scrubber.css @@ -21,7 +21,7 @@ THE SOFTWARE. */ .Scrubber--handle { - cursor: ew-resize; + cursor: col-resize; fill: #555; } diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.css b/src/components/TracePage/SpanGraph/ViewingLayer.css index 5b4d2cc747..5c24072c1e 100644 --- a/src/components/TracePage/SpanGraph/ViewingLayer.css +++ b/src/components/TracePage/SpanGraph/ViewingLayer.css @@ -24,6 +24,7 @@ THE SOFTWARE. position: relative; z-index: 1; margin-bottom: 0.5em; + cursor: vertical-text; } .ViewingLayer--graph { @@ -48,21 +49,22 @@ THE SOFTWARE. fill-opacity: 0.2; } -.ViewingLayer--draggedShift.isScrubberDrag, -.ViewingLayer--draggedEdge.isScrubberDrag { +.ViewingLayer--draggedShift.isShiftDrag, +.ViewingLayer--draggedEdge.isShiftDrag { fill: #44f; } -.ViewingLayer--draggedShift.isResetDrag, -.ViewingLayer--draggedEdge.isResetDrag { +.ViewingLayer--draggedShift.isReframeDrag, +.ViewingLayer--draggedEdge.isReframeDrag { fill: #f44; } .ViewingLayer--fullOverlay { bottom: 0; - cursor: ew-resize; + cursor: col-resize; left: 0; position: fixed; right: 0; top: 0; + user-select: none; } diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.js b/src/components/TracePage/SpanGraph/ViewingLayer.js index 8edf02c706..6a5ea5d3bc 100644 --- a/src/components/TracePage/SpanGraph/ViewingLayer.js +++ b/src/components/TracePage/SpanGraph/ViewingLayer.js @@ -22,34 +22,44 @@ import * as React from 'react'; import cx from 'classnames'; +import _clamp from 'lodash/clamp'; -import MouseDraggedState from './MouseDraggedState'; import GraphTicks from './GraphTicks'; import Scrubber from './Scrubber'; +import { NextViewRangeTypes } from '../types'; +import type { NextViewRangeType, ViewRange } from '../types'; import './ViewingLayer.css'; type ViewingLayerProps = { height: number, numTicks: number, - updateViewRange: ([number, number]) => void, - viewRange: [number, number], + updateViewRange: (number, number) => void, + updateNextViewRange: (number, number, NextViewRangeType) => void, + viewRange: ViewRange, }; type ViewingLayerState = { cursorX: ?number, - draggedState: ?MouseDraggedState, preventCursorLine: boolean, }; const LEFT_MOUSE_BUTTON = 0; -const dragTags = { - RESET: 'RESET', - SCRUB_INTERMEDIATE_STATE: 'SCRUBBER_INTERMEDIATE_STATE', - SCRUB_LEFT_HANDLE: 'SCRUB_LEFT_HANDLE', - SCRUB_RIGHT_HANDLE: 'SCRUB_RIGHT_HANDLE', -}; +function getNextViewLayout(start: number, position: number) { + if (start < position) { + return { + x: `${start * 100}%`, + width: `${(position - start) * 100}%`, + leadingX: `${position * 100}%`, + }; + } + return { + x: `${position * 100}%`, + width: `${(start - position) * 100}%`, + leadingX: `${position * 100}%`, + }; +} export default class ViewingLayer extends React.PureComponent { props: ViewingLayerProps; @@ -65,7 +75,6 @@ export default class ViewingLayer extends React.PureComponent) => { - this._handleScrubberMouseDown(dragTags.SCRUB_LEFT_HANDLE, event); + this._handleScrubberMouseDown(NextViewRangeTypes.SHIFT_LEFT, event); }; this._handleRightScrubberMouseDown = (event: SyntheticMouseEvent) => { - this._handleScrubberMouseDown(dragTags.SCRUB_RIGHT_HANDLE, event); + this._handleScrubberMouseDown(NextViewRangeTypes.SHIFT_RIGHT, event); }; this._handleRootMouseMove = this._handleRootMouseMove.bind(this); this._handleRootMouseLeave = this._handleRootMouseLeave.bind(this); @@ -97,15 +106,31 @@ export default class ViewingLayer extends React.PureComponent) { + _handleScrubberMouseDown(type: NextViewRangeType, event: SyntheticMouseEvent) { const { button, clientX } = event; if (button !== LEFT_MOUSE_BUTTON) { return; @@ -115,125 +140,98 @@ export default class ViewingLayer extends React.PureComponent) { if (this._rootClientRect) { - this.setState({ ...this.state, cursorX: clientX - this._rootClientRect.left }); + this.setState({ cursorX: clientX - this._rootClientRect.left }); } }; _handleRootMouseLeave = function _handleRootMouseLeave() { - this.setState({ ...this.state, cursorX: undefined }); + this.setState({ cursorX: undefined }); }; _handleRootMouseDown = function _handleRootMouseDown({ button, clientX }: SyntheticMouseEvent) { if (!this._root || button !== LEFT_MOUSE_BUTTON) { return; } - // the ClientRect retrieved when the SVG is initially rendered has an - // inaccurate width, so refresh the ClientRect on mouse down - this._rootClientRect = this._root.getBoundingClientRect(); window.addEventListener('mousemove', this._handleWindowMouseMove); window.addEventListener('mouseup', this._handleWindowMouseUp); this._windowListenersAttached = true; - const draggedState = MouseDraggedState.newFromOptions({ - clientX, - clientRect: this._rootClientRect, - max: 1, - min: 0, - tag: dragTags.RESET, - }); - this.setState({ ...this.state, draggedState }); + // the ClientRect retrieved when the SVG is initially rendered has an + // inaccurate width, so refresh the ClientRect on mouse down + this._rootClientRect = this._root.getBoundingClientRect(); + const position = this._getPosition(clientX, NextViewRangeTypes.REFRAME); + this.props.updateNextViewRange(position, position, NextViewRangeTypes.REFRAME); }; _handleWindowMouseMove = function _handleWindowMouseMove({ clientX }: SyntheticMouseEvent) { - if (this.state.draggedState) { - const draggedState = this.state.draggedState.newPositionFromClientX(clientX); - this.setState({ ...this.state, draggedState }); + if (!this._root || !this.props.viewRange.next) { + return; } + const { start, type } = this.props.viewRange.next; + const position = this._getPosition(clientX, type); + this.props.updateNextViewRange(start, position, type); }; _handleWindowMouseUp = function _handleWindowMouseUp({ clientX }: SyntheticMouseEvent) { window.removeEventListener('mousemove', this._handleWindowMouseMove); window.removeEventListener('mouseup', this._handleWindowMouseUp); this._windowListenersAttached = false; - if (this.state.draggedState) { - const draggedState = this.state.draggedState.newPositionFromClientX(clientX); - const { start: draggedFrom, position: draggedTo } = draggedState; - let viewStart; - let viewEnd; - if (draggedState.tag === dragTags.RESET) { - if (draggedFrom < draggedTo) { - viewStart = draggedFrom; - viewEnd = draggedTo; - } else { - viewStart = draggedTo; - viewEnd = draggedFrom; - } - } else if (draggedState.tag === dragTags.SCRUB_LEFT_HANDLE) { - const [, currentViewEnd] = this.props.viewRange; - viewStart = draggedTo; - viewEnd = currentViewEnd; - } else { - const [currentViewStart] = this.props.viewRange; - viewStart = currentViewStart; - viewEnd = draggedTo; - } - this.props.updateViewRange(viewStart, viewEnd); - // reset cursorX to prevent a remnant cursorX from missing the mouseleave - // event - this.setState({ ...this.state, cursorX: undefined, draggedState: undefined, preventCursorLine: false }); + if (!this._root || !this.props.viewRange.next) { + return; + } + const { start, type } = this.props.viewRange.next; + const position = this._getPosition(clientX, type); + if (type === NextViewRangeTypes.REFRAME) { + const [newStart, newEnd] = start < position ? [start, position] : [position, start]; + this.props.updateViewRange(newStart, newEnd); + return; + } + if (type === NextViewRangeTypes.SHIFT_LEFT) { + const [, viewEnd] = this.props.viewRange.current; + this.props.updateViewRange(position, viewEnd); + return; } + const [viewStart] = this.props.viewRange.current; + this.props.updateViewRange(viewStart, position); }; render() { const { height, viewRange, numTicks } = this.props; - const { cursorX, draggedState, preventCursorLine } = this.state; - const [leftBound, rightBound] = viewRange; + const { cursorX, preventCursorLine } = this.state; + const [viewStart, viewEnd] = viewRange.current; + // const + console.log('viewRange:', viewRange); let leftInactive = 0; - if (leftBound) { - leftInactive = leftBound * 100; + if (viewStart) { + leftInactive = viewStart * 100; } let rightInactive = 100; - if (rightBound) { - rightInactive = 100 - rightBound * 100; + if (viewEnd) { + rightInactive = 100 - viewEnd * 100; } - let isScrubberDrag = false; - const dragTag = draggedState && draggedState.tag; + let isShiftDrag = false; let dragMarkers: ?(React.Node[]); let fullOverlay: ?React.Node; let cursorGuide: ?React.Node; - if (draggedState && dragTag) { - isScrubberDrag = dragTag !== dragTags.RESET; + if (viewRange.next) { + const { start, position, type } = viewRange.next; + isShiftDrag = type !== NextViewRangeTypes.REFRAME; const cls = cx({ - isScrubberDrag, - isResetDrag: !isScrubberDrag, + isShiftDrag, + isReframeDrag: !isShiftDrag, }); - const layout = draggedState.getLayout(); + const layout = getNextViewLayout(start, position); dragMarkers = [ } {cursorGuide} - {isScrubberDrag && dragMarkers} + {isShiftDrag && dragMarkers} - {!isScrubberDrag && dragMarkers} + {!isShiftDrag && dragMarkers} {fullOverlay}
diff --git a/src/components/TracePage/SpanGraph/index.js b/src/components/TracePage/SpanGraph/index.js index 68c9cb9a5e..86ceaaf1da 100644 --- a/src/components/TracePage/SpanGraph/index.js +++ b/src/components/TracePage/SpanGraph/index.js @@ -25,6 +25,7 @@ import * as React from 'react'; import CanvasSpanGraph from './CanvasSpanGraph'; import TickLabels from './TickLabels'; import ViewingLayer from './ViewingLayer'; +import type { NextViewRangeType, ViewRange } from '../types'; import type { Trace } from '../../../types'; const TIMELINE_TICK_INTERVAL = 4; @@ -32,12 +33,13 @@ const TIMELINE_TICK_INTERVAL = 4; type SpanGraphProps = { height: number, trace: Trace, - viewRange: [number, number], - updateViewRange: ([number, number]) => void, + viewRange: ViewRange, + updateViewRange: (number, number) => void, + updateNextViewRange: (number, number, NextViewRangeType) => void, }; export default function SpanGraph(props: SpanGraphProps) { - const { height, trace, viewRange, updateViewRange } = props; + const { height, trace, viewRange, updateNextViewRange, updateViewRange } = props; if (!trace) { return
; @@ -59,6 +61,7 @@ export default function SpanGraph(props: SpanGraphProps) { numTicks={TIMELINE_TICK_INTERVAL} height={height} updateViewRange={updateViewRange} + updateNextViewRange={updateNextViewRange} />
diff --git a/src/components/TracePage/TracePageHeader.js b/src/components/TracePage/TracePageHeader.js index 3b30f79952..ab7a5c3a47 100644 --- a/src/components/TracePage/TracePageHeader.js +++ b/src/components/TracePage/TracePageHeader.js @@ -52,9 +52,8 @@ export const HEADER_ITEMS = [ }, ]; -export default function TracePageHeader(props, context) { - const { traceID, name, slimView, onSlimViewClicked } = props; - const { updateTextFilter, textFilter } = context; +export default function TracePageHeader(props) { + const { traceID, name, slimView, onSlimViewClicked, updateTextFilter, textFilter } = props; if (!traceID) { return null; @@ -115,18 +114,15 @@ export default function TracePageHeader(props, context) { } TracePageHeader.propTypes = { - traceID: PropTypes.string, - name: PropTypes.string, + duration: PropTypes.number, // eslint-disable-line react/no-unused-prop-types maxDepth: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + name: PropTypes.string, numServices: PropTypes.number, // eslint-disable-line react/no-unused-prop-types numSpans: PropTypes.number, // eslint-disable-line react/no-unused-prop-types - duration: PropTypes.number, // eslint-disable-line react/no-unused-prop-types - timestamp: PropTypes.number, // eslint-disable-line react/no-unused-prop-types - slimView: PropTypes.bool, onSlimViewClicked: PropTypes.func, -}; - -TracePageHeader.contextTypes = { - textFilter: PropTypes.string.isRequired, + slimView: PropTypes.bool, + textFilter: PropTypes.string, + timestamp: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + traceID: PropTypes.string, updateTextFilter: PropTypes.func.isRequired, }; diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.css b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.css index b8a69fa2f9..5516521c91 100644 --- a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.css +++ b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.css @@ -37,7 +37,7 @@ THE SOFTWARE. .TimelineColumnResizer--dragger { border-left: 1px solid transparent; bottom: 0; - cursor: ew-resize; + cursor: col-resize; position: fixed; top: 0; width: 5px; diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.js b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.js index 3fe0e5a013..9abb735a41 100644 --- a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.js +++ b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.js @@ -125,7 +125,7 @@ class TimelineColumnResizer extends React.PureComponent< window.removeEventListener('mouseup', this._onWindowMouseUp); const style = _get(document, 'body.style'); if (style) { - (style: any).userSelect = undefined; + (style: any).userSelect = null; } this._isDragging = false; const dragPosition = this._getDraggedPosition(clientX); diff --git a/src/components/TracePage/TraceTimelineViewer/index.js b/src/components/TracePage/TraceTimelineViewer/index.js index 1b3d4dd60a..39b664910e 100644 --- a/src/components/TracePage/TraceTimelineViewer/index.js +++ b/src/components/TracePage/TraceTimelineViewer/index.js @@ -30,19 +30,19 @@ import './index.css'; type TraceTimelineViewerProps = { trace: ?Trace, - timeRangeFilter: [number, number], + currentViewRange: [number, number], textFilter: ?string, }; export default function TraceTimelineViewer(props: TraceTimelineViewerProps) { - const { timeRangeFilter: zoomRange, textFilter, trace } = props; + const { currentViewRange, textFilter, trace } = props; return (
); diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index 28f8d47d3a..51e4529013 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -1,3 +1,5 @@ +// @flow + // Copyright (c) 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,74 +20,67 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React, { Component } from 'react'; +import * as React from 'react'; import _maxBy from 'lodash/maxBy'; import _values from 'lodash/values'; -import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import TracePageHeader from './TracePageHeader'; import SpanGraph from './SpanGraph'; import TraceTimelineViewer from './TraceTimelineViewer'; +import type { NextViewRangeType, ViewRange } from './types'; import NotFound from '../App/NotFound'; import * as jaegerApiActions from '../../actions/jaeger-api'; import { getTraceName } from '../../model/trace-viewer'; +import type { Trace } from '../../types'; import colorGenerator from '../../utils/color-generator'; import './index.css'; -export default class TracePage extends Component { - static get propTypes() { - return { - fetchTrace: PropTypes.func.isRequired, - trace: PropTypes.object, - loading: PropTypes.bool, - id: PropTypes.string.isRequired, - }; - } +type TracePageProps = { + fetchTrace: string => void, + trace: ?Trace, + loading: boolean, + id: string, +}; - static get childContextTypes() { - return { - textFilter: PropTypes.string, - updateTextFilter: PropTypes.func, - updateTimeRangeFilter: PropTypes.func, - slimView: PropTypes.bool, - }; - } +type TracePageState = { + textFilter: ?string, + viewRange: ViewRange, + slimView: boolean, + headerHeight: ?number, +}; + +export default class TracePage extends React.PureComponent { + props: TracePageProps; + state: TracePageState; - constructor(props) { + headerElm: ?Element; + + constructor(props: TracePageProps) { super(props); this.state = { textFilter: '', - timeRangeFilter: [], + viewRange: { current: [0, 1] }, slimView: false, headerHeight: null, }; this.headerElm = null; this.setHeaderHeight = this.setHeaderHeight.bind(this); this.toggleSlimView = this.toggleSlimView.bind(this); - this.updateTimeRangeFilter = this.updateTimeRangeFilter.bind(this); - } - - getChildContext() { - const state = { ...this.state }; - delete state.headerHeight; - delete state.timeRangeFilter; - return { - updateTextFilter: this.updateTextFilter.bind(this), - updateTimeRangeFilter: this.updateTimeRangeFilter, - ...state, - }; + this.updateViewRange = this.updateViewRange.bind(this); + this.updateNextViewRange = this.updateNextViewRange.bind(this); + this.updateTextFilter = this.updateTextFilter.bind(this); } componentDidMount() { colorGenerator.clear(); this.ensureTraceFetched(); - this.setDefaultTimeRange(); + this.updateViewRange(0, 1); } - componentDidUpdate({ trace: prevTrace }) { + componentDidUpdate({ trace: prevTrace }: TracePageProps) { const { trace } = this.props; this.setHeaderHeight(this.headerElm); if (!trace) { @@ -93,11 +88,11 @@ export default class TracePage extends Component { return; } if (!(trace instanceof Error) && (!prevTrace || prevTrace.traceID !== trace.traceID)) { - this.setDefaultTimeRange(); + this.updateViewRange(0, 1); } } - setHeaderHeight(elm) { + setHeaderHeight = function setHeaderHeight(elm: ?Element) { this.headerElm = elm; if (elm) { if (this.state.headerHeight !== elm.clientHeight) { @@ -106,28 +101,31 @@ export default class TracePage extends Component { } else if (this.state.headerHeight) { this.setState({ headerHeight: null }); } - } + }; - setDefaultTimeRange() { - const { trace } = this.props; - if (!trace) { - this.updateTimeRangeFilter(null, null); - return; - } - this.updateTimeRangeFilter(0, 1); - } - - updateTextFilter(textFilter) { + updateTextFilter = function updateTextFilter(textFilter: ?string) { this.setState({ textFilter }); - } - - updateTimeRangeFilter(...timeRangeFilter) { - this.setState({ timeRangeFilter }); - } - - toggleSlimView() { + }; + + updateViewRange = function updateViewRange(start: number, end: number) { + const viewRange = { current: [start, end] }; + this.setState({ viewRange }); + }; + + updateNextViewRange = function updateNextViewRange( + start: number, + position: number, + type: NextViewRangeType + ) { + console.log('next view range', start, position, type); + const { current } = this.state.viewRange; + const viewRange = { current, next: { start, position, type } }; + this.setState({ viewRange }); + }; + + toggleSlimView = function toggleSlimView() { this.setState({ slimView: !this.state.slimView }); - } + }; ensureTraceFetched() { const { fetchTrace, trace, id, loading } = this.props; @@ -139,7 +137,7 @@ export default class TracePage extends Component { render() { const { id, loading, trace } = this.props; - const { slimView, headerHeight } = this.state; + const { slimView, headerHeight, textFilter, viewRange } = this.state; if (!trace) { if (loading) { @@ -172,21 +170,20 @@ export default class TracePage extends Component { timestamp={startTime} traceID={traceID} onSlimViewClicked={this.toggleSlimView} + textFilter={textFilter} + updateTextFilter={this.updateTextFilter} /> {!slimView && }
{headerHeight &&
- +
}
); diff --git a/src/components/TracePage/types.js b/src/components/TracePage/types.js new file mode 100644 index 0000000000..1fac16139e --- /dev/null +++ b/src/components/TracePage/types.js @@ -0,0 +1,34 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +export const NextViewRangeTypes = { + REFRAME: 'REFRAME', + SHIFT_LEFT: 'SHIFT_LEFT', + SHIFT_RIGHT: 'SHIFT_RIGHT', +}; + +export type NextViewRangeType = $Keys; + +export type ViewRange = { + current: [number, number], + next?: { start: number, position: number, type: NextViewRangeType }, +}; From ada62fe6901e115a425201b0762d77c3a9b04d32 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Tue, 3 Oct 2017 00:16:45 -0400 Subject: [PATCH 05/20] Pan, scroll, zoom shortcuts; clearer minimap - Color generator now also provides colors in RGB - ListView now has a public API to inspect the state of the ListView scrolling - VirtualizedTraceView now has a public API to inspect the state of the trace view - SpanGraph scrubbers have wider grips - SpanGraph minimap rendering update so the spans of large traces are more visible - Tween utility class added - scroll-page util added for animating window scrolling - ScrollManager added for scrolling to prev / next visible span, sensitive to search filtering - Keyboard shortcuts added for panning left, right, up down; zoom in, out; fast pan, zoom; skip to prev / next visible span TODO - Fix tests, add tests - Reorganize a bit to make things a bit smaller and more compartmentalized (a few things are getting a little large) - Refactor viewRange.next to support animating next position for both scrubbers --- package.json | 6 +- src/components/TracePage/ScrollManager.js | 204 ++++++++++++++++++ .../TracePage/SpanGraph/CanvasSpanGraph.js | 2 +- .../TracePage/SpanGraph/Scrubber.css | 27 ++- .../TracePage/SpanGraph/Scrubber.js | 25 ++- .../TracePage/SpanGraph/ViewingLayer.js | 5 +- .../TracePage/SpanGraph/render-into-canvas.js | 11 +- .../TraceTimelineViewer/ListView/Positions.js | 64 +++--- .../TraceTimelineViewer/ListView/index.js | 63 +++--- .../VirtualizedTraceView.js | 142 +++++++++--- .../TracePage/TraceTimelineViewer/index.js | 14 +- src/components/TracePage/Tween.js | 115 ++++++++++ src/components/TracePage/index.js | 112 ++++++++-- .../TracePage/keyboard-shortcuts.js | 86 ++++++++ src/components/TracePage/scroll-page.js | 64 ++++++ src/utils/color-generator.js | 64 ++++-- yarn.lock | 8 + 17 files changed, 865 insertions(+), 147 deletions(-) create mode 100644 src/components/TracePage/ScrollManager.js create mode 100644 src/components/TracePage/Tween.js create mode 100644 src/components/TracePage/keyboard-shortcuts.js create mode 100644 src/components/TracePage/scroll-page.js diff --git a/package.json b/package.json index b6e60066e1..48c01d8c25 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "basscss": "^8.0.3", "chance": "^1.0.10", "classnames": "^2.2.5", + "combokeys": "^3.0.0", "cytoscape": "^3.2.1", "cytoscape-dagre": "^2.0.0", "d3-scale": "^1.0.6", @@ -65,7 +66,8 @@ "reselect": "^3.0.1", "semantic-ui-css": "^2.2.12", "semantic-ui-react": "^0.71.4", - "store": "^2.0.12" + "store": "^2.0.12", + "tween-functions": "^1.2.0" }, "scripts": { "start": "react-scripts start", @@ -83,7 +85,7 @@ "clear-homepage": "json -I -f package.json -e 'delete this.homepage'", "deploy-docs": "./bin/deploy-docs.sh", "postpublish": "npm run build:docs && npm run deploy-docs", - "add-license": "uber-licence --dir src", + "add-license": "uber-licence && git co flow-typed", "precommit": "lint-staged" }, "lint-staged": { diff --git a/src/components/TracePage/ScrollManager.js b/src/components/TracePage/ScrollManager.js new file mode 100644 index 0000000000..686577f783 --- /dev/null +++ b/src/components/TracePage/ScrollManager.js @@ -0,0 +1,204 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import type { Span, Trace } from '../../types'; + +export type Accessors = { + getViewRange: () => [number, number], + getSearchedSpanIDs: () => ?Set, + getCollapsedChildren: () => ?Set, + getViewHeight: () => number, + getBottomRowIndexVisible: () => number, + getTopRowIndexVisible: () => number, + getRowPosition: number => { height: number, y: number }, + mapRowIndexToSpanIndex: number => number, + mapSpanIndexToRowIndex: number => number, +}; + +interface Scroller { + scrollTo: number => void, + scrollBy: number => void, +} + +function isSpanHidden(span: Span, childrenAreHidden: Set, spansMap: Map) { + const parentIDs = new Set(); + let { references } = span; + let parentID: ?string; + const checkRef = ref => { + if (ref.refType === 'CHILD_OF') { + parentID = ref.spanID; + parentIDs.add(parentID); + return childrenAreHidden.has(parentID); + } + return false; + }; + while (Array.isArray(references) && references.length) { + const isHidden = references.some(checkRef); + if (isHidden) { + return { isHidden, parentIDs }; + } + if (!parentID) { + break; + } + const parent = spansMap.get(parentID); + parentID = undefined; + references = parent && parent.references; + } + return { parentIDs, isHidden: false }; +} + +export default class ScrollManager { + _trace: ?Trace; + _scroller: Scroller; + _accessors: ?Accessors; + + constructor(trace: ?Trace, scroller: Scroller) { + this._trace = trace; + this._scroller = scroller; + this._accessors = undefined; + + this.scrollToNextVisibleSpan = this.scrollToNextVisibleSpan.bind(this); + this.scrollToPrevVisibleSpan = this.scrollToPrevVisibleSpan.bind(this); + this.scrollPageDown = this.scrollPageDown.bind(this); + this.scrollPageUp = this.scrollPageUp.bind(this); + this.setAccessors = this.setAccessors.bind(this); + } + + _scrollPast(rowIndex: number, direction: 1 | -1) { + const xrs = this._accessors; + if (!xrs) { + throw new Error('Accessors not set'); + } + const isUp = direction < 0; + const position = xrs.getRowPosition(rowIndex); + if (!position) { + console.warn('Invalid row index'); + return; + } + let { y } = position; + const vh = xrs.getViewHeight(); + if (!isUp) { + y += position.height; + // scrollTop is based on the top of the window + y -= vh; + } + y += direction * 0.5 * vh; + this._scroller.scrollTo(y); + } + + _scrollToVisibleSpan(direction: 1 | -1) { + const xrs = this._accessors; + if (!xrs) { + throw new Error('Accessors not set'); + } + if (!this._trace) { + return; + } + const { duration, spans, startTime: traceStartTime } = this._trace; + const isUp = direction < 0; + const boundaryRow = isUp ? xrs.getTopRowIndexVisible() : xrs.getBottomRowIndexVisible(); + const spanIndex = xrs.mapRowIndexToSpanIndex(boundaryRow); + if ((spanIndex === 0 && isUp) || (spanIndex === spans.length - 1 && !isUp)) { + return; + } + // fullViewSpanIndex is one row inside the view window + const fullViewSpanIndex = spanIndex - direction; + const [viewStart, viewEnd] = xrs.getViewRange(); + const checkVisibility = viewStart !== 0 || viewEnd !== 1; + // use NaN as fallback to make flow happy + const startTime = checkVisibility ? traceStartTime + duration * viewStart : NaN; + const endTime = checkVisibility ? traceStartTime + duration * viewEnd : NaN; + const findMatches = xrs.getSearchedSpanIDs(); + const _collapsed = xrs.getCollapsedChildren(); + const childrenAreHidden = _collapsed ? new Set(_collapsed) : null; + // use empty Map as fallback to make flow happy + const spansMap = childrenAreHidden ? new Map(spans.map(s => [s.spanID, s])) : new Map(); + const boundary = direction < 0 ? -1 : spans.length; + let nextSpanIndex: number; + for (let i = fullViewSpanIndex + direction; i !== boundary; i += direction) { + const span = spans[i]; + const { duration: spanDuration, spanID, startTime: spanStartTime } = span; + const spanEndTime = spanStartTime + spanDuration; + if (checkVisibility && (spanStartTime > endTime || spanEndTime < startTime)) { + // span is not visible within the view range + continue; + } + if (findMatches && !findMatches.has(spanID)) { + // skip to search matches (when searching) + continue; + } + if (childrenAreHidden) { + // make sure the span is not collapsed + const { isHidden, parentIDs } = isSpanHidden(span, childrenAreHidden, spansMap); + if (isHidden) { + childrenAreHidden.add(...parentIDs); + continue; + } + } + nextSpanIndex = i; + break; + } + if (!nextSpanIndex || nextSpanIndex === boundary) { + // might as well scroll to the top or bottom + nextSpanIndex = boundary - direction; + } + const nextRow = xrs.mapSpanIndexToRowIndex(nextSpanIndex); + this._scrollPast(nextRow, direction); + } + + setTrace(trace: ?Trace) { + this._trace = trace; + } + + setAccessors = function setAccessors(accessors: Accessors) { + this._accessors = accessors; + }; + + scrollPageDown = function scrollPageDown() { + if (!this._scroller || !this._accessors) { + return; + } + this._scroller.scrollBy(0.95 * this._accessors.getViewHeight(), true); + }; + + scrollPageUp = function scrollPageUp() { + if (!this._scroller || !this._accessors) { + return; + } + this._scroller.scrollBy(-0.95 * this._accessors.getViewHeight(), true); + }; + + scrollToNextVisibleSpan = function scrollToNextVisibleSpan() { + this._scrollToVisibleSpan(1); + }; + + scrollToPrevVisibleSpan = function scrollToPrevVisibleSpan() { + this._scrollToVisibleSpan(-1); + }; + + destroy() { + // eslint-disable-next-line no-unused-vars + this._trace = undefined; + this._scroller = (undefined: any); + this._accessors = undefined; + } +} diff --git a/src/components/TracePage/SpanGraph/CanvasSpanGraph.js b/src/components/TracePage/SpanGraph/CanvasSpanGraph.js index eb36e5f573..d5e05d8616 100644 --- a/src/components/TracePage/SpanGraph/CanvasSpanGraph.js +++ b/src/components/TracePage/SpanGraph/CanvasSpanGraph.js @@ -34,7 +34,7 @@ type CanvasSpanGraphProps = { const CV_WIDTH = 4000; -const getColor = str => colorGenerator.getColorByKey(str); +const getColor: string => [number, number, number] = str => colorGenerator.getRgbColorByKey(str); export default class CanvasSpanGraph extends React.PureComponent { props: CanvasSpanGraphProps; diff --git a/src/components/TracePage/SpanGraph/Scrubber.css b/src/components/TracePage/SpanGraph/Scrubber.css index d9f9d61ae9..dc48a568e1 100644 --- a/src/components/TracePage/SpanGraph/Scrubber.css +++ b/src/components/TracePage/SpanGraph/Scrubber.css @@ -20,22 +20,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +.Scrubber--handleExpansion { + cursor: col-resize; + fill-opacity: 0; + fill: #44f; +} + + .Scrubber.isDragging .Scrubber--handleExpansion, + .Scrubber--handles:hover > .Scrubber--handleExpansion { + fill-opacity: 1; + } + .Scrubber--handle { cursor: col-resize; fill: #555; } -.Scrubber.isDragging > .Scrubber--handle, -.Scrubber--handle:hover { - fill: #44f; -} + .Scrubber.isDragging .Scrubber--handle, + .Scrubber--handles:hover > .Scrubber--handle { + fill: #44f; + } .Scrubber--line { pointer-events: none; stroke: #555; } -.Scrubber.isDragging > .Scrubber--line, -.Scrubber--handle:hover + .Scrubber--line { - stroke: #44f; -} + .Scrubber.isDragging > .Scrubber--line, + .Scrubber--handles:hover + .Scrubber--line { + stroke: #44f; + } diff --git a/src/components/TracePage/SpanGraph/Scrubber.js b/src/components/TracePage/SpanGraph/Scrubber.js index b1e942029a..ba13915a92 100644 --- a/src/components/TracePage/SpanGraph/Scrubber.js +++ b/src/components/TracePage/SpanGraph/Scrubber.js @@ -44,16 +44,27 @@ export default function Scrubber({ const className = cx('Scrubber', { isDragging }); return ( - + > + + + ); diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.js b/src/components/TracePage/SpanGraph/ViewingLayer.js index 6a5ea5d3bc..5f7b4c70e4 100644 --- a/src/components/TracePage/SpanGraph/ViewingLayer.js +++ b/src/components/TracePage/SpanGraph/ViewingLayer.js @@ -202,6 +202,9 @@ export default class ViewingLayer extends React.PureComponent string + getFillColor: string => [number, number, number] ) { // eslint-disable-next-line no-param-reassign canvas.width = CV_WIDTH; let itemHeight = 1; + let itemYChange = 1; if (items.length < MIN_TOTAL_HEIGHT) { // eslint-disable-next-line no-param-reassign canvas.height = MIN_TOTAL_HEIGHT; itemHeight = MIN_TOTAL_HEIGHT / items.length; + itemYChange = MIN_TOTAL_HEIGHT / items.length; } else { // eslint-disable-next-line no-param-reassign canvas.height = items.length; - itemHeight = 1; + itemYChange = 1; + itemHeight = 1 / (MIN_TOTAL_HEIGHT / items.length); } const ctx = canvas.getContext('2d'); for (let i = 0; i < items.length; i++) { @@ -52,7 +55,7 @@ export default function renderIntoCanvas( if (width < MIN_WIDTH) { width = MIN_WIDTH; } - ctx.fillStyle = getFillColor(serviceName); - ctx.fillRect(x, i * itemHeight, width, itemHeight); + ctx.fillStyle = `rgba(${getFillColor(serviceName).concat(0.3).join()})`; + ctx.fillRect(x, i * itemYChange, width, itemHeight); } } diff --git a/src/components/TracePage/TraceTimelineViewer/ListView/Positions.js b/src/components/TracePage/TraceTimelineViewer/ListView/Positions.js index bfc630771d..ec300f2e5b 100644 --- a/src/components/TracePage/TraceTimelineViewer/ListView/Positions.js +++ b/src/components/TracePage/TraceTimelineViewer/ListView/Positions.js @@ -108,6 +108,34 @@ export default class Positions { } } + /** + * Get the latest height for index `_i`. If it's in new terretory + * (_i > lastI), find the heights (and y-values) leading up to it. If it's in + * known territory (_i <= lastI) and the height is different than what is + * known, recalculate subsequent y values, but don't confirm the heights of + * those items, just update based on the difference. + */ + confirmHeight(_i: number, heightGetter: number => number) { + let i = _i; + if (i > this.lastI) { + this.calcHeights(i, heightGetter); + return; + } + const h = heightGetter(i); + if (h === this.heights[i]) { + return; + } + const chg = h - this.heights[i]; + this.heights[i] = h; + // shift the y positions by `chg` for all known y positions + while (++i <= this.lastI) { + this.ys[i] += chg; + } + if (this.ys[this.lastI + 1] != null) { + this.ys[this.lastI + 1] += chg; + } + } + /** * Given a target y-value (`yValue`), find the closest index (in the `.ys` * array) that is prior to the y-value; e.g. map from y-value to index in @@ -145,6 +173,14 @@ export default class Positions { throw new Error(`unable to find floor index for y=${yValue}`); } + getRowPosition(index: number, heightGetter: number => number) { + this.confirmHeight(index, heightGetter); + return { + height: this.heights[index], + y: this.ys[index], + }; + } + /** * Get the estimated height of the whole shebang by extrapolating based on * the average known height. @@ -158,32 +194,4 @@ export default class Positions { // eslint-disable-next-line no-bitwise return (known / (this.lastI + 1) * this.heights.length) | 0; } - - /** - * Get the latest height for index `_i`. If it's in new terretory - * (_i > lastI), find the heights (and y-values) leading up to it. If it's in - * known territory (_i <= lastI) and the height is different than what is - * known, recalculate subsequent y values, but don't confirm the heights of - * those items, just update based on the difference. - */ - confirmHeight(_i: number, heightGetter: number => number) { - let i = _i; - if (i > this.lastI) { - this.calcHeights(i, heightGetter); - return; - } - const h = heightGetter(i); - if (h === this.heights[i]) { - return; - } - const chg = h - this.heights[i]; - this.heights[i] = h; - // shift the y positions by `chg` for all known y positions - while (++i <= this.lastI) { - this.ys[i] += chg; - } - if (this.ys[this.lastI + 1] != null) { - this.ys[this.lastI + 1] += chg; - } - } } diff --git a/src/components/TracePage/TraceTimelineViewer/ListView/index.js b/src/components/TracePage/TraceTimelineViewer/ListView/index.js index 18516d4d19..562a201876 100644 --- a/src/components/TracePage/TraceTimelineViewer/ListView/index.js +++ b/src/components/TracePage/TraceTimelineViewer/ListView/index.js @@ -178,11 +178,20 @@ export default class ListView extends React.Component { constructor(props: ListViewProps) { super(props); + this.getViewHeight = this.getViewHeight.bind(this); + this.getBottomVisibleIndex = this.getBottomVisibleIndex.bind(this); + this.getTopVisibleIndex = this.getTopVisibleIndex.bind(this); + this.getRowPosition = this.getRowPosition.bind(this); + this._getHeight = this._getHeight.bind(this); + this._scanItemHeights = this._scanItemHeights.bind(this); + this._onScroll = this._onScroll.bind(this); + this._positionList = this._positionList.bind(this); + this._initWrapper = this._initWrapper.bind(this); + this._initItemHolder = this._initItemHolder.bind(this); + this._yPositions = new Positions(200); // _knownHeights is (item-key -> observed height) of list items this._knownHeights = new Map(); - this._getHeight = this._getHeight.bind(this); - this._scanItemHeights = this._scanItemHeights.bind(this); this._startIndexDrawn = 2 ** 20; this._endIndexDrawn = -(2 ** 20); @@ -192,17 +201,12 @@ export default class ListView extends React.Component { this._scrollTop = -1; this._isScrolledOrResized = false; - this._onScroll = this._onScroll.bind(this); - this._positionList = this._positionList.bind(this); - this._htmlTopOffset = -1; this._windowScrollListenerAdded = false; // _htmlElm is only relevant if props.windowScroller is true - this._htmlElm = window.document.querySelector('html'); + this._htmlElm = (document.documentElement: any); this._wrapperElm = undefined; this._itemHolderElm = undefined; - this._initWrapper = this._initWrapper.bind(this); - this._initItemHolder = this._initItemHolder.bind(this); } componentDidMount() { @@ -228,6 +232,23 @@ export default class ListView extends React.Component { } } + getViewHeight = function getViewHeight(): number { + return this._viewHeight; + }; + + getBottomVisibleIndex = function getBottomVisibleIndex(): number { + const bottomY = this._scrollTop + this._viewHeight; + return this._yPositions.findFloorIndex(bottomY); + }; + + getTopVisibleIndex = function getTopVisibleIndex(): number { + return this._yPositions.findFloorIndex(this._scrollTop, this._getHeight); + }; + + getRowPosition = function getRowPosition(index: number): { height: number, y: number } { + return this._yPositions.getRowPosition(index, this._getHeight); + }; + /** * Scroll event listener that schedules a remeasuring of which items should be * rendered. @@ -269,23 +290,11 @@ export default class ListView extends React.Component { this._viewHeight = this._wrapperElm.clientHeight; this._scrollTop = this._wrapperElm.scrollTop; } else { - this._viewHeight = this._htmlElm.clientHeight; - this._scrollTop = this._htmlElm.scrollTop; - } - let yStart; - let yEnd; - if (useRoot) { - if (this._scrollTop < this._htmlTopOffset) { - yStart = 0; - yEnd = this._viewHeight - this._htmlTopOffset + this._scrollTop; - } else { - yStart = this._scrollTop - this._htmlTopOffset; - yEnd = yStart + this._viewHeight; - } - } else { - yStart = this._scrollTop; - yEnd = this._scrollTop + this._viewHeight; + this._viewHeight = window.innerHeight - this._htmlTopOffset; + this._scrollTop = window.scrollY; } + const yStart = this._scrollTop; + const yEnd = this._scrollTop + this._viewHeight; this._startIndex = this._yPositions.findFloorIndex(yStart, this._getHeight); this._endIndex = this._yPositions.findFloorIndex(yEnd, this._getHeight); } @@ -432,11 +441,11 @@ export default class ListView extends React.Component { items.length = end - start + 1; for (let i = start; i <= end; i++) { - this._yPositions.confirmHeight(i, heightGetter); + const { y: top, height } = this._yPositions.getRowPosition(i, heightGetter); const style = { + height, + top, position: 'absolute', - top: this._yPositions.ys[i], - height: this._yPositions.heights[i], }; const itemKey = getKeyFromIndex(i); const attrs = { 'data-item-key': itemKey }; diff --git a/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js b/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js index eb766d6765..0584dcc4d2 100644 --- a/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js +++ b/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js @@ -38,6 +38,7 @@ import { isErrorSpan, spanContainsErredSpan, } from './utils'; +import type { Accessors } from '../ScrollManager'; import type { Log, Span, Trace } from '../../../types'; import colorGenerator from '../../../utils/color-generator'; @@ -52,6 +53,7 @@ type RowState = { type VirtualizedTraceViewProps = { childrenHiddenIDs: Set, childrenToggle: string => void, + currentViewRange: [number, number], detailLogItemToggle: (string, Log) => void, detailLogsToggle: string => void, detailProcessToggle: string => void, @@ -60,13 +62,12 @@ type VirtualizedTraceViewProps = { detailToggle: string => void, find: (?Trace, ?string) => void, findMatchesIDs: Set, + registerAccessors: Accessors => void, setTrace: (?string) => void, setSpanNameColumnWidth: number => void, spanNameColumnWidth: number, textFilter: ?string, - trace?: Trace, - zoomEnd: number, - zoomStart: number, + trace: Trace, }; const DEFAULT_HEIGHTS = { @@ -120,20 +121,12 @@ function generateRowStates( return rowStates; } -function getPropDerivations(props: VirtualizedTraceViewProps) { - const { childrenHiddenIDs, detailStates, trace, zoomEnd = 1, zoomStart = 0 } = props; - const clippingCssClasses = cx({ +function getCssClasses(viewRange) { + const [zoomStart, zoomEnd] = viewRange; + return cx({ 'clipping-left': zoomStart > 0, 'clipping-right': zoomEnd < 1, }); - let spans: ?(Span[]); - if (trace) { - spans = trace.spans; - } - return { - clippingCssClasses, - rowStates: generateRowStates(spans, childrenHiddenIDs, detailStates), - }; } class VirtualizedTraceView extends React.PureComponent { @@ -142,20 +135,27 @@ class VirtualizedTraceView extends React.PureComponent
void, textFilter: ?string, + trace: Trace, }; +// TODO(joe): remove this component + export default function TraceTimelineViewer(props: TraceTimelineViewerProps) { - const { currentViewRange, textFilter, trace } = props; return (
- +
); } diff --git a/src/components/TracePage/Tween.js b/src/components/TracePage/Tween.js new file mode 100644 index 0000000000..54dec0bc3c --- /dev/null +++ b/src/components/TracePage/Tween.js @@ -0,0 +1,115 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import ease from 'tween-functions'; + +type TweenCallback = ({ done: boolean, value: number }) => void; + +type TweenOptions = { + delay?: number, + duration: number, + from: number, + onComplete?: TweenCallback, + onUpdate?: TweenCallback, + to: number, +}; + +export default class Tween { + callbackComplete: ?TweenCallback; + callbackUpdate: ?TweenCallback; + delay: number; + duration: number; + from: number; + requestID: ?number; + startTime: number; + timeoutID: ?number; + to: number; + + constructor({ duration, from, to, delay, onUpdate, onComplete }: TweenOptions) { + this.startTime = Date.now() + (delay || 0); + this.duration = duration; + this.from = from; + this.to = to; + if (!onUpdate && !onComplete) { + this.callbackComplete = undefined; + this.callbackUpdate = undefined; + this.timeoutID = undefined; + this.requestID = undefined; + } else { + this._frameCallback = this._frameCallback.bind(this); + this.callbackComplete = onComplete; + this.callbackUpdate = onUpdate; + if (delay) { + this.timeoutID = setTimeout(this._frameCallback, delay); + this.requestID = undefined; + } else { + this.requestID = window.requestAnimationFrame(this._frameCallback); + this.timeoutID = undefined; + } + } + } + + _frameCallback = function _frameCallback() { + this.timeoutID = undefined; + this.requestID = undefined; + const current = Object.freeze(this.getCurrent()); + if (this.callbackUpdate) { + this.callbackUpdate(current); + } + if (this.callbackComplete && current.done) { + this.callbackComplete(current); + } + if (current.done) { + this.callbackComplete = undefined; + this.callbackUpdate = undefined; + } else { + this.requestID = window.requestAnimationFrame(this._frameCallback); + } + }; + + cancel() { + if (this.timeoutID != null) { + clearTimeout(this.timeoutID); + this.timeoutID = undefined; + } + if (this.requestID != null) { + window.cancelAnimationFrame(this.requestID); + this.requestID = undefined; + } + this.callbackComplete = undefined; + this.callbackUpdate = undefined; + } + + getCurrent(): { done: boolean, value: number } { + const t = Date.now() - this.startTime; + if (t <= 0) { + // still in the delay period + return { done: false, value: this.from }; + } + if (t >= this.duration) { + // after the expiration + return { done: true, value: this.to }; + } + // mid-tween + return { done: false, value: ease.easeOutQuint(t, this.from, this.to, this.duration) }; + } +} diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index 51e4529013..cdc373769a 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -21,13 +21,19 @@ // THE SOFTWARE. import * as React from 'react'; +import _clamp from 'lodash/clamp'; +import _mapKeys from 'lodash/mapKeys'; import _maxBy from 'lodash/maxBy'; import _values from 'lodash/values'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import TracePageHeader from './TracePageHeader'; +import type { CombokeysHandler, ShortcutCallbacks } from './keyboard-shortcuts'; +import { init as initShortcuts, reset as resetShortcuts } from './keyboard-shortcuts'; +import { cancel as cancelPageScroll, scrollBy, scrollTo } from './scroll-page'; +import ScrollManager from './ScrollManager'; import SpanGraph from './SpanGraph'; +import TracePageHeader from './TracePageHeader'; import TraceTimelineViewer from './TraceTimelineViewer'; import type { NextViewRangeType, ViewRange } from './types'; import NotFound from '../App/NotFound'; @@ -52,37 +58,87 @@ type TracePageState = { headerHeight: ?number, }; +const VIEW_MIN_RANGE = 0.01; +const VIEW_CHANGE_BASE = 0.005; +const VIEW_CHANGE_FAST = 0.05; + +const shortcutConfig = { + panLeft: [-VIEW_CHANGE_BASE, -VIEW_CHANGE_BASE], + panLeftFast: [-VIEW_CHANGE_FAST, -VIEW_CHANGE_FAST], + panRight: [VIEW_CHANGE_BASE, VIEW_CHANGE_BASE], + panRightFast: [VIEW_CHANGE_FAST, VIEW_CHANGE_FAST], + zoomIn: [VIEW_CHANGE_BASE, -VIEW_CHANGE_BASE], + zoomInFast: [VIEW_CHANGE_FAST, -VIEW_CHANGE_FAST], + zoomOut: [-VIEW_CHANGE_BASE, VIEW_CHANGE_BASE], + zoomOutFast: [-VIEW_CHANGE_FAST, VIEW_CHANGE_FAST], +}; + +function makeShortcutCallbacks(adjRange): ShortcutCallbacks { + const callbacks: { [string]: CombokeysHandler } = {}; + _mapKeys(shortcutConfig, ([startChange, endChange], key) => { + callbacks[key] = (event: SyntheticKeyboardEvent) => { + event.preventDefault(); + adjRange(startChange, endChange); + }; + }); + return (callbacks: any); +} + export default class TracePage extends React.PureComponent { props: TracePageProps; state: TracePageState; - headerElm: ?Element; + _headerElm: ?Element; + _scrollManager: ScrollManager; constructor(props: TracePageProps) { super(props); + this.setHeaderHeight = this.setHeaderHeight.bind(this); + this.toggleSlimView = this.toggleSlimView.bind(this); + this.updateViewRange = this.updateViewRange.bind(this); + this.updateNextViewRange = this.updateNextViewRange.bind(this); + this.updateTextFilter = this.updateTextFilter.bind(this); this.state = { textFilter: '', viewRange: { current: [0, 1] }, slimView: false, headerHeight: null, }; - this.headerElm = null; - this.setHeaderHeight = this.setHeaderHeight.bind(this); - this.toggleSlimView = this.toggleSlimView.bind(this); - this.updateViewRange = this.updateViewRange.bind(this); - this.updateNextViewRange = this.updateNextViewRange.bind(this); - this.updateTextFilter = this.updateTextFilter.bind(this); + this._headerElm = null; + this._scrollManager = new ScrollManager(props.trace, { scrollBy, scrollTo }); } componentDidMount() { colorGenerator.clear(); this.ensureTraceFetched(); this.updateViewRange(0, 1); + if (!this._scrollManager) { + throw new Error('Invalid state - scrollManager is unset'); + } + const { + scrollPageDown, + scrollPageUp, + scrollToNextVisibleSpan, + scrollToPrevVisibleSpan, + } = this._scrollManager; + const adjViewRange = (a: number, b: number) => this._adjustViewRange(a, b); + const shortcutCallbacks = makeShortcutCallbacks(adjViewRange); + shortcutCallbacks.scrollPageDown = scrollPageDown; + shortcutCallbacks.scrollPageUp = scrollPageUp; + shortcutCallbacks.scrollToNextVisibleSpan = scrollToNextVisibleSpan; + shortcutCallbacks.scrollToPrevVisibleSpan = scrollToPrevVisibleSpan; + initShortcuts(shortcutCallbacks); + } + + componentWillReceiveProps(nextProps: TracePageProps) { + if (this._scrollManager) { + this._scrollManager.setTrace(nextProps.trace); + } } componentDidUpdate({ trace: prevTrace }: TracePageProps) { const { trace } = this.props; - this.setHeaderHeight(this.headerElm); + this.setHeaderHeight(this._headerElm); if (!trace) { this.ensureTraceFetched(); return; @@ -92,8 +148,35 @@ export default class TracePage extends React.PureComponent 0 && endChange > 0) { + end = start + VIEW_MIN_RANGE; + } else { + const center = viewStart + (viewEnd - viewStart) / 2; + start = center - VIEW_MIN_RANGE / 2; + end = center + VIEW_MIN_RANGE / 2; + } + } + this.updateViewRange(start, end); + } + setHeaderHeight = function setHeaderHeight(elm: ?Element) { - this.headerElm = elm; + this._headerElm = elm; if (elm) { if (this.state.headerHeight !== elm.clientHeight) { this.setState({ headerHeight: elm.clientHeight }); @@ -117,7 +200,6 @@ export default class TracePage extends React.PureComponent {headerHeight &&
- +
}
); diff --git a/src/components/TracePage/keyboard-shortcuts.js b/src/components/TracePage/keyboard-shortcuts.js new file mode 100644 index 0000000000..2302fab1c5 --- /dev/null +++ b/src/components/TracePage/keyboard-shortcuts.js @@ -0,0 +1,86 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Combokeys from 'combokeys'; + +export type CombokeysHandler = + | (() => void) + | ((SyntheticKeyboardEvent) => void) + | ((SyntheticKeyboardEvent, string) => void); + +type CombokeysType = { + bind: (string | string[], CombokeysHandler) => void, + reset: () => void, +}; + +export type ShortcutCallbacks = { + scrollPageDown: CombokeysHandler, + scrollPageUp: CombokeysHandler, + scrollToNextVisibleSpan: CombokeysHandler, + scrollToPrevVisibleSpan: CombokeysHandler, + // view range + panLeft: CombokeysHandler, + panLeftFast: CombokeysHandler, + panRight: CombokeysHandler, + panRightFast: CombokeysHandler, + zoomIn: CombokeysHandler, + zoomInFast: CombokeysHandler, + zoomOut: CombokeysHandler, + zoomOutFast: CombokeysHandler, +}; + +const kbdMappings = { + scrollPageDown: 's', + scrollPageUp: 'w', + scrollToNextVisibleSpan: 'f', + scrollToPrevVisibleSpan: 'b', + panLeft: ['a', 'left'], + panLeftFast: ['shift+a', 'shift+left'], + panRight: ['d', 'right'], + panRightFast: ['shift+d', 'shift+right'], + zoomIn: 'up', + zoomInFast: 'shift+up', + zoomOut: 'down', + zoomOutFast: 'shift+down', +}; + +let instance: ?CombokeysType; + +function getInstance(): CombokeysType { + if (!instance) { + instance = new Combokeys(document.body); + } + return instance; +} + +export function init(callbacks: ShortcutCallbacks) { + const combokeys = getInstance(); + combokeys.reset(); + Object.keys(kbdMappings).forEach(name => { + combokeys.bind(kbdMappings[name], callbacks[name]); + }); +} + +export function reset() { + const combokeys = getInstance(); + combokeys.reset(); +} diff --git a/src/components/TracePage/scroll-page.js b/src/components/TracePage/scroll-page.js new file mode 100644 index 0000000000..f1c14d89e9 --- /dev/null +++ b/src/components/TracePage/scroll-page.js @@ -0,0 +1,64 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Tween from './Tween'; + +const DURATION_MS = 350; + +let lastTween: ?Tween; + +// TODO(joe): this util can be modified a bit to be generalized (e.g. take in +// an element as a parameter and use scrollTop instead of window.scrollTo) + +function _onTweenUpdate({ done, value }: { done: boolean, value: number }) { + window.scrollTo(window.scrollX, value); + if (done) { + lastTween = null; + } +} + +export function scrollBy(yDelta: number, appendToLast: boolean = false) { + const { scrollY } = window; + let targetFrom = scrollY; + if (appendToLast && lastTween) { + // if `append` and we will be scrolling in the same direction as lastTween + const currentDirection = lastTween.to < scrollY ? 'up' : 'down'; + const nextDirection = yDelta < 0 ? 'up' : 'down'; + if (currentDirection === nextDirection) { + targetFrom = lastTween.to; + } + } + const to = targetFrom + yDelta; + lastTween = new Tween({ to, duration: DURATION_MS, from: scrollY, onUpdate: _onTweenUpdate }); +} + +export function scrollTo(y: number) { + const { scrollY } = window; + lastTween = new Tween({ duration: DURATION_MS, from: scrollY, to: y, onUpdate: _onTweenUpdate }); +} + +export function cancel() { + if (lastTween) { + lastTween.cancel(); + lastTween = undefined; + } +} diff --git a/src/utils/color-generator.js b/src/utils/color-generator.js index ec8e038964..82711e3b40 100644 --- a/src/utils/color-generator.js +++ b/src/utils/color-generator.js @@ -1,3 +1,5 @@ +// @flow + // Copyright (c) 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -const DEFAULT_COLORS = [ +const COLORS_HEX = [ '#17B8BE', '#F8DCA1', '#B7885E', @@ -41,12 +43,44 @@ const DEFAULT_COLORS = [ '#776E57', ]; +function mapHexToRgb(colors): [number, number, number][] { + const hexRegex = /\w\w/g; + return colors.map(s => { + const _s = s.slice(1); + const rv: number[] = []; + let match = hexRegex.exec(_s); + while (match) { + const hex = match[0]; + const b10 = parseInt(hex, 16); + rv.push(b10); + match = hexRegex.exec(_s); + } + return Object.freeze((rv: any)); + }); +} + export class ColorGenerator { - constructor(colorPalette = DEFAULT_COLORS) { - this.colors = colorPalette; + colorsHex: string[]; + colorsRgb: [number, number, number][]; + cache: Map; + currentIdx: number; + + constructor(colorsHex: string[] = COLORS_HEX) { + this.colorsHex = colorsHex; + this.colorsRgb = mapHexToRgb(colorsHex); this.cache = new Map(); this.currentIdx = 0; } + + _getColorIndex(key: string): number { + let i = this.cache.get(key); + if (i == null) { + i = this.currentIdx; + this.cache.set(key, this.currentIdx); + this.currentIdx = ++this.currentIdx % this.colorsHex.length; + } + return i; + } /** * Will assign a color to an arbitrary key. * If the key has been used already, it will @@ -55,17 +89,21 @@ export class ColorGenerator { * @param {String} key Key name * @return {String} HEX Color */ - getColorByKey(key) { - const cache = this.cache; - if (!cache.has(key)) { - cache.set(key, this.colors[this.currentIdx]); - this.currentIdx++; - if (this.currentIdx >= this.colors.length) { - this.currentIdx = 0; - } - } - return cache.get(key); + getColorByKey(key: string) { + const i = this._getColorIndex(key); + return this.colorsHex[i]; } + + /** + * Retrieve the RGB values associated with a key. Adds the key and associates + * it with a color if the key is not recognized. + * @return {number[]} An array of three ints [0, 255] representing a color. + */ + getRgbColorByKey(key: string): [number, number, number] { + const i = this._getColorIndex(key); + return this.colorsRgb[i]; + } + clear() { this.cache.clear(); this.currentIdx = 0; diff --git a/yarn.lock b/yarn.lock index b4f769cbba..cfd991b829 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1557,6 +1557,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +combokeys@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/combokeys/-/combokeys-3.0.0.tgz#955c59a3959af40d26846ab6fc3c682448e7572e" + commander@2.11.x, commander@^2.9.0, commander@~2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -7098,6 +7102,10 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tween-functions@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" From c2745de1e7ac2da6dd35dc731c1394eadebecd74 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Wed, 4 Oct 2017 03:55:17 -0400 Subject: [PATCH 06/20] Restructure components/TracePage#state.viewRange --- .../TracePage/SpanGraph/MouseDraggedState.js | 83 ------- .../TracePage/SpanGraph/ViewingLayer.js | 235 +++++++++--------- src/components/TracePage/SpanGraph/index.js | 8 +- .../TracePage/TracePageHeader.test.js | 22 +- src/components/TracePage/index.js | 54 ++-- src/components/TracePage/scroll-page.js | 1 - src/components/TracePage/types.js | 38 ++- 7 files changed, 200 insertions(+), 241 deletions(-) delete mode 100644 src/components/TracePage/SpanGraph/MouseDraggedState.js diff --git a/src/components/TracePage/SpanGraph/MouseDraggedState.js b/src/components/TracePage/SpanGraph/MouseDraggedState.js deleted file mode 100644 index c8e36de5a4..0000000000 --- a/src/components/TracePage/SpanGraph/MouseDraggedState.js +++ /dev/null @@ -1,83 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import _clamp from 'lodash/clamp'; - -export default class MouseDraggedState { - clientRect: ClientRect; - max: number; - min: number; - position: number; - start: number; - tag: string; - - static newFromOptions(options: { - clientRect: ClientRect, - clientX: number, - max?: number, - min?: number, - start?: number, - tag: string, - }) { - const opts = {}; - opts.clientRect = options.clientRect; - opts.max = options.max == null ? 1 : options.max; - opts.min = options.min == null ? 0 : options.min; - opts.position = (options.clientX - opts.clientRect.left) / (opts.clientRect.width || 1); - opts.start = options.start == null ? opts.position : options.start; - opts.tag = options.tag; - return new MouseDraggedState(opts); - } - - constructor(options: { - clientRect: ClientRect, - max: number, - min: number, - position: number, - start: number, - tag: string, - }) { - Object.assign(this, options); - } - - newPositionFromClientX(clientX: number) { - const position = (clientX - this.clientRect.left) / (this.clientRect.width || 1); - const next = new MouseDraggedState(this); - next.position = _clamp(position, this.min, this.max); - return next; - } - - getLayout() { - if (this.position < this.start) { - return { - x: `${this.position * 100}%`, - width: `${(this.start - this.position) * 100}%`, - leadingX: `${this.position * 100}%`, - }; - } - return { - x: `${this.start * 100}%`, - width: `${(this.position - this.start) * 100}%`, - leadingX: `${this.position * 100}%`, - }; - } -} diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.js b/src/components/TracePage/SpanGraph/ViewingLayer.js index 5f7b4c70e4..9b43a6cb6a 100644 --- a/src/components/TracePage/SpanGraph/ViewingLayer.js +++ b/src/components/TracePage/SpanGraph/ViewingLayer.js @@ -26,16 +26,17 @@ import _clamp from 'lodash/clamp'; import GraphTicks from './GraphTicks'; import Scrubber from './Scrubber'; -import { NextViewRangeTypes } from '../types'; -import type { NextViewRangeType, ViewRange } from '../types'; +import type { ViewRange, ViewRangeTimeUpdate } from '../types'; import './ViewingLayer.css'; +type DragType = 'SHIFT_END' | 'SHIFT_START' | 'REFRAME'; + type ViewingLayerProps = { height: number, numTicks: number, updateViewRange: (number, number) => void, - updateNextViewRange: (number, number, NextViewRangeType) => void, + updateNextViewRangeTime: ViewRangeTimeUpdate => void, viewRange: ViewRange, }; @@ -46,17 +47,17 @@ type ViewingLayerState = { const LEFT_MOUSE_BUTTON = 0; +const dragTypes = { + SHIFT_END: 'SHIFT_END', + SHIFT_START: 'SHIFT_START', + REFRAME: 'REFRAME', +}; + function getNextViewLayout(start: number, position: number) { - if (start < position) { - return { - x: `${start * 100}%`, - width: `${(position - start) * 100}%`, - leadingX: `${position * 100}%`, - }; - } + const [left, right] = start < position ? [start, position] : [position, start]; return { - x: `${position * 100}%`, - width: `${(start - position) * 100}%`, + x: `${left * 100}%`, + width: `${(right - left) * 100}%`, leadingX: `${position * 100}%`, }; } @@ -68,33 +69,35 @@ export default class ViewingLayer extends React.PureComponent) => void; _handleRightScrubberMouseDown: (SyntheticMouseEvent) => void; constructor(props: ViewingLayerProps) { super(props); - this.state = { - cursorX: undefined, - preventCursorLine: false, - }; - this._root = undefined; - this._rootClientRect = undefined; - this._windowListenersAttached = false; - this._setRoot = this._setRoot.bind(this); this._handleScrubberMouseEnter = this._handleScrubberMouseEnter.bind(this); this._handleScrubberMouseLeave = this._handleScrubberMouseLeave.bind(this); this._handleLeftScrubberMouseDown = (event: SyntheticMouseEvent) => { - this._handleScrubberMouseDown(NextViewRangeTypes.SHIFT_LEFT, event); + this._handleScrubberMouseDown(dragTypes.SHIFT_START, event); }; this._handleRightScrubberMouseDown = (event: SyntheticMouseEvent) => { - this._handleScrubberMouseDown(NextViewRangeTypes.SHIFT_RIGHT, event); + this._handleScrubberMouseDown(dragTypes.SHIFT_END, event); }; this._handleRootMouseMove = this._handleRootMouseMove.bind(this); this._handleRootMouseLeave = this._handleRootMouseLeave.bind(this); this._handleRootMouseDown = this._handleRootMouseDown.bind(this); this._handleWindowMouseMove = this._handleWindowMouseMove.bind(this); this._handleWindowMouseUp = this._handleWindowMouseUp.bind(this); + + this.state = { + cursorX: undefined, + preventCursorLine: false, + }; + this._root = undefined; + this._rootClientRect = undefined; + this._windowListenersAttached = false; + this._currentDragType = undefined; } _setRoot = function _setRoot(elm: ?Element) { @@ -106,20 +109,28 @@ export default class ViewingLayer extends React.PureComponent) { + _handleScrubberMouseDown(type: DragType, event: SyntheticMouseEvent) { const { button, clientX } = event; if (button !== LEFT_MOUSE_BUTTON) { return; @@ -141,13 +152,14 @@ export default class ViewingLayer extends React.PureComponent) { @@ -164,55 +176,87 @@ export default class ViewingLayer extends React.PureComponent) { - if (!this._root || !this.props.viewRange.next) { + if (!this._root || !this._currentDragType) { return; } - const { start, type } = this.props.viewRange.next; - const position = this._getPosition(clientX, type); - this.props.updateNextViewRange(start, position, type); + const update = this._getUpdate(clientX, this._currentDragType); + this.props.updateNextViewRangeTime(update); }; _handleWindowMouseUp = function _handleWindowMouseUp({ clientX }: SyntheticMouseEvent) { window.removeEventListener('mousemove', this._handleWindowMouseMove); window.removeEventListener('mouseup', this._handleWindowMouseUp); this._windowListenersAttached = false; - if (!this._root || !this.props.viewRange.next) { + if (!this._root || !this._currentDragType) { return; } - const { start, type } = this.props.viewRange.next; - const position = this._getPosition(clientX, type); - if (type === NextViewRangeTypes.REFRAME) { - const [newStart, newEnd] = start < position ? [start, position] : [position, start]; - this.props.updateViewRange(newStart, newEnd); + const type = this._currentDragType; + this._currentDragType = undefined; + // reset cursorX to prevent a remnant cursorX from missing the mouseleave + // event + this.setState({ cursorX: undefined, preventCursorLine: false }); + + const update = this._getUpdate(clientX, type); + if (type === dragTypes.REFRAME) { + const { reframe: { anchor, shift } } = update; + const [start, end] = anchor < shift ? [anchor, shift] : [shift, anchor]; + this.props.updateViewRange(start, end); return; } - if (type === NextViewRangeTypes.SHIFT_LEFT) { - const [, viewEnd] = this.props.viewRange.current; - this.props.updateViewRange(position, viewEnd); + const [viewStart, viewEnd] = this.props.viewRange.time.current; + if (type === dragTypes.SHIFT_START) { + this.props.updateViewRange(update.shiftStart, viewEnd); return; } - // reset cursorX to prevent a remnant cursorX from missing the mouseleave - // event - this.setState({ cursorX: undefined, preventCursorLine: false }); - const [viewStart] = this.props.viewRange.current; - this.props.updateViewRange(viewStart, position); + this.props.updateViewRange(viewStart, update.shiftEnd); }; + getMarkers(from: number, to: number, isShift: boolean) { + const layout = getNextViewLayout(from, to); + const cls = cx({ + isShiftDrag: isShift, + isReframeDrag: !isShift, + }); + return [ + , + , + ]; + } + render() { const { height, viewRange, numTicks } = this.props; const { cursorX, preventCursorLine } = this.state; - const [viewStart, viewEnd] = viewRange.current; + const { current, shiftStart, shiftEnd, reframe } = viewRange.time; + const haveNextTimeRange = shiftStart != null || shiftEnd != null || reframe != null; + const [viewStart, viewEnd] = current; let leftInactive = 0; if (viewStart) { leftInactive = viewStart * 100; @@ -221,49 +265,7 @@ export default class ViewingLayer extends React.PureComponent, - , - ]; - fullOverlay =
; - } else if (cursorX != null && !preventCursorLine) { - cursorGuide = ( - - ); - } + const drawCursor = !haveNextTimeRange && cursorX != null && !preventCursorLine; return (
} - {cursorGuide} - {isShiftDrag && dragMarkers} + {drawCursor && + } + {shiftStart != null && this.getMarkers(viewStart, shiftStart, true)} + {shiftEnd != null && this.getMarkers(viewEnd, shiftEnd, true)} - {!isShiftDrag && dragMarkers} + {reframe != null && this.getMarkers(reframe.anchor, reframe.shift, false)} - {fullOverlay} + {haveNextTimeRange &&
}
); } diff --git a/src/components/TracePage/SpanGraph/index.js b/src/components/TracePage/SpanGraph/index.js index 86ceaaf1da..78af909359 100644 --- a/src/components/TracePage/SpanGraph/index.js +++ b/src/components/TracePage/SpanGraph/index.js @@ -25,7 +25,7 @@ import * as React from 'react'; import CanvasSpanGraph from './CanvasSpanGraph'; import TickLabels from './TickLabels'; import ViewingLayer from './ViewingLayer'; -import type { NextViewRangeType, ViewRange } from '../types'; +import type { ViewRange, ViewRangeTimeUpdate } from '../types'; import type { Trace } from '../../../types'; const TIMELINE_TICK_INTERVAL = 4; @@ -35,11 +35,11 @@ type SpanGraphProps = { trace: Trace, viewRange: ViewRange, updateViewRange: (number, number) => void, - updateNextViewRange: (number, number, NextViewRangeType) => void, + updateNextViewRangeTime: ViewRangeTimeUpdate => void, }; export default function SpanGraph(props: SpanGraphProps) { - const { height, trace, viewRange, updateNextViewRange, updateViewRange } = props; + const { height, trace, viewRange, updateNextViewRangeTime, updateViewRange } = props; if (!trace) { return
; @@ -61,7 +61,7 @@ export default function SpanGraph(props: SpanGraphProps) { numTicks={TIMELINE_TICK_INTERVAL} height={height} updateViewRange={updateViewRange} - updateNextViewRange={updateNextViewRange} + updateNextViewRangeTime={updateNextViewRangeTime} />
diff --git a/src/components/TracePage/TracePageHeader.test.js b/src/components/TracePage/TracePageHeader.test.js index 1c64837392..8389fcf473 100644 --- a/src/components/TracePage/TracePageHeader.test.js +++ b/src/components/TracePage/TracePageHeader.test.js @@ -28,19 +28,14 @@ describe('', () => { const defaultProps = { traceID: 'some-trace-id', name: 'some-trace-name', - }; - - const defaultOptions = { - context: { - textFilter: '', - updateTextFilter: () => {}, - }, + textFilter: '', + updateTextFilter: () => {}, }; let wrapper; beforeEach(() => { - wrapper = shallow(, defaultOptions); + wrapper = shallow(); }); it('renders a
', () => { @@ -48,7 +43,7 @@ describe('', () => { }); it('renders an empty
if no traceID is present', () => { - wrapper = mount(, defaultOptions); + wrapper = mount(); expect(wrapper.children().length).toBe(0); }); @@ -66,13 +61,8 @@ describe('', () => { it('calls the context updateTextFilter() function for onChange of the input', () => { const updateTextFilter = sinon.spy(); - wrapper = shallow(, { - ...defaultOptions, - context: { - ...defaultOptions.context, - updateTextFilter, - }, - }); + const props = { ...defaultProps, updateTextFilter }; + wrapper = shallow(); const event = { target: { value: 'my new value' } }; wrapper.find('#trace-page__text-filter').first().prop('onChange')(event); expect(updateTextFilter.calledWith('my new value')).toBeTruthy(); diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index cdc373769a..8053dd0da0 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -35,7 +35,7 @@ import ScrollManager from './ScrollManager'; import SpanGraph from './SpanGraph'; import TracePageHeader from './TracePageHeader'; import TraceTimelineViewer from './TraceTimelineViewer'; -import type { NextViewRangeType, ViewRange } from './types'; +import type { ViewRange, ViewRangeTimeUpdate } from './types'; import NotFound from '../App/NotFound'; import * as jaegerApiActions from '../../actions/jaeger-api'; import { getTraceName } from '../../model/trace-viewer'; @@ -52,10 +52,10 @@ type TracePageProps = { }; type TracePageState = { + headerHeight: ?number, + slimView: boolean, textFilter: ?string, viewRange: ViewRange, - slimView: boolean, - headerHeight: ?number, }; const VIEW_MIN_RANGE = 0.01; @@ -96,13 +96,25 @@ export default class TracePage extends React.PureComponent} {headerHeight &&
; +type TimeShiftEndUpdate = { + shiftEnd: number, +}; + +type TimeShiftStartUpdate = { + shiftStart: number, +}; + +export type ViewRangeTimeUpdate = TimeReframeUpdate | TimeShiftEndUpdate | TimeShiftStartUpdate; export type ViewRange = { - current: [number, number], - next?: { start: number, position: number, type: NextViewRangeType }, + time: { + current: [number, number], + reframe?: { + anchor: number, + shift: number, + }, + shiftEnd?: number, + shiftStart?: number, + }, + rows: { + bottom: number, + top: number, + }, + spans: { + bottom: number, + top: number, + }, }; From d2cd51897410e28aa44f7ea7df584ce5d51216e4 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 5 Oct 2017 08:59:22 -0400 Subject: [PATCH 07/20] Perf improved when expanding row details (~60%) --- .../TraceTimelineViewer/SpanBarRow.js | 215 ++++++++++-------- .../TraceTimelineViewer/SpanDetailRow.js | 96 ++++---- .../VirtualizedTraceView.js | 12 +- src/components/TracePage/index.js | 4 +- 4 files changed, 177 insertions(+), 150 deletions(-) diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js index 5ccae96224..01a0e44a97 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React from 'react'; +import * as React from 'react'; import TimelineRow from './TimelineRow'; import SpanTreeOffset from './SpanTreeOffset'; @@ -39,8 +39,8 @@ type SpanBarRowProps = { isFilteredOut: boolean, isParent: boolean, label: string, - onDetailToggled: () => void, - onChildrenToggled: () => void, + onDetailToggled: string => void, + onChildrenToggled: string => void, operationName: string, numTicks: number, rpc: ?{ @@ -52,107 +52,124 @@ type SpanBarRowProps = { }, serviceName: string, showErrorIcon: boolean, + spanID: string, viewEnd: number, viewStart: number, }; -export default function SpanBarRow(props: SpanBarRowProps) { - const { - className, - color, - columnDivision, - depth, - isChildrenExpanded, - isDetailExapnded, - isFilteredOut, - isParent, - label, - onDetailToggled, - onChildrenToggled, - numTicks, - operationName, - rpc, - serviceName, - showErrorIcon, - viewEnd, - viewStart, - } = props; +export default class SpanBarRow extends React.PureComponent { + props: SpanBarRowProps; - const labelDetail = `${serviceName}::${operationName}`; - let longLabel; - let hintSide; - if (viewStart > 1 - viewEnd) { - longLabel = `${labelDetail} | ${label}`; - hintSide = 'left'; - } else { - longLabel = `${label} | ${labelDetail}`; - hintSide = 'right'; + static defaultProps = { + className: '', + rpc: null, + }; + + constructor(props: SpanBarRowProps) { + super(props); + this._detailToggle = this._detailToggle.bind(this); + this._childrenToggle = this._childrenToggle.bind(this); } - return ( - - -