diff --git a/src/components/TracePage/SpanGraph/CanvasSpanGraph.css b/src/components/TracePage/SpanGraph/CanvasSpanGraph.css new file mode 100644 index 0000000000..8d5eb7b7be --- /dev/null +++ b/src/components/TracePage/SpanGraph/CanvasSpanGraph.css @@ -0,0 +1,28 @@ +/* +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. +*/ + +.CanvasSpanGraph { + background: #fafafa; + height: 60px; + position: absolute; + width: 100%; +} diff --git a/src/components/TracePage/SpanGraph/CanvasSpanGraph.js b/src/components/TracePage/SpanGraph/CanvasSpanGraph.js new file mode 100644 index 0000000000..eb36e5f573 --- /dev/null +++ b/src/components/TracePage/SpanGraph/CanvasSpanGraph.js @@ -0,0 +1,78 @@ +// @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 renderIntoCanvas from './render-into-canvas'; +import colorGenerator from '../../../utils/color-generator'; + +import './CanvasSpanGraph.css'; + +type CanvasSpanGraphProps = { + items: { valueWidth: number, valueOffset: number, serviceName: string }[], + valueWidth: number, +}; + +const CV_WIDTH = 4000; + +const getColor = str => colorGenerator.getColorByKey(str); + +export default class CanvasSpanGraph extends React.PureComponent { + props: CanvasSpanGraphProps; + _canvasElm: ?HTMLCanvasElement; + + constructor(props: CanvasSpanGraphProps) { + super(props); + this._canvasElm = undefined; + this._setCanvasRef = this._setCanvasRef.bind(this); + } + + componentDidMount() { + this._draw(); + } + + componentDidUpdate() { + this._draw(); + } + + _setCanvasRef = function _setCanvasRef(elm: React.Node) { + this._canvasElm = elm; + }; + + _draw() { + if (this._canvasElm) { + const { valueWidth: totalValueWidth, items } = this.props; + renderIntoCanvas(this._canvasElm, items, totalValueWidth, getColor); + } + } + + render() { + return ( + + ); + } +} diff --git a/src/components/TracePage/SpanGraph/GraphTicks.css b/src/components/TracePage/SpanGraph/GraphTicks.css new file mode 100644 index 0000000000..52f5055bf8 --- /dev/null +++ b/src/components/TracePage/SpanGraph/GraphTicks.css @@ -0,0 +1,26 @@ +/* +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. +*/ + +.GraphTick { + stroke: #aaa; + stroke-width: 1px; +} diff --git a/src/components/TracePage/SpanGraph/GraphTicks.js b/src/components/TracePage/SpanGraph/GraphTicks.js new file mode 100644 index 0000000000..020c8dfd33 --- /dev/null +++ b/src/components/TracePage/SpanGraph/GraphTicks.js @@ -0,0 +1,45 @@ +// @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 './GraphTicks.css'; + +type GraphTicksProps = { + numTicks: number, +}; + +export default function GraphTicks(props: GraphTicksProps) { + const { numTicks } = props; + const ticks = []; + // i starts at 1, limit is `i < numTicks` so the first and last ticks aren't drawn + for (let i = 1; i < numTicks; i++) { + const x = `${i / numTicks * 100}%`; + ticks.push(); + } + + return ( + + ); +} diff --git a/src/components/TracePage/SpanGraph/GraphTicks.test.js b/src/components/TracePage/SpanGraph/GraphTicks.test.js new file mode 100644 index 0000000000..cd6f343382 --- /dev/null +++ b/src/components/TracePage/SpanGraph/GraphTicks.test.js @@ -0,0 +1,50 @@ +// 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 { shallow } from 'enzyme'; + +import GraphTicks from './GraphTicks'; + +describe('', () => { + const defaultProps = { + items: [ + { valueWidth: 100, valueOffset: 25, serviceName: 'a' }, + { valueWidth: 100, valueOffset: 50, serviceName: 'b' }, + ], + valueWidth: 200, + numTicks: 4, + }; + + let ticksG; + + beforeEach(() => { + const wrapper = shallow(); + ticksG = wrapper.find('[data-test="ticks"]'); + }); + + it('creates a for ticks', () => { + expect(ticksG.length).toBe(1); + }); + + it('creates a line for each ticks excluding the first and last', () => { + expect(ticksG.find('line').length).toBe(defaultProps.numTicks - 1); + }); +}); diff --git a/src/components/TracePage/SpanGraph/Scrubber.css b/src/components/TracePage/SpanGraph/Scrubber.css new file mode 100644 index 0000000000..accdda7bbd --- /dev/null +++ b/src/components/TracePage/SpanGraph/Scrubber.css @@ -0,0 +1,50 @@ +/* +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. +*/ + +.timeline-scrubber { + cursor: ew-resize; +} + +.timeline-scrubber__line { + stroke: #999; + stroke-width: 1; +} + +.timeline-scrubber:hover .timeline-scrubber__line { + stroke: #777; +} + +.timeline-scrubber__handle { + 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; +} diff --git a/src/components/TracePage/TimelineScrubber.js b/src/components/TracePage/SpanGraph/Scrubber.js similarity index 63% rename from src/components/TracePage/TimelineScrubber.js rename to src/components/TracePage/SpanGraph/Scrubber.js index fe4b951899..16f2008206 100644 --- a/src/components/TracePage/TimelineScrubber.js +++ b/src/components/TracePage/SpanGraph/Scrubber.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,39 +20,35 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import PropTypes from 'prop-types'; import React from 'react'; -import { getTraceTimestamp, getTraceDuration } from '../../selectors/trace'; -import { getPercentageOfInterval } from '../../utils/date'; +import './Scrubber.css'; + +type ScrubberProps = { + position: number, + onMouseDown: (SyntheticMouseEvent) => void, + handleTopOffset: number, + handleWidth: number, + handleHeight: number, +}; const HANDLE_WIDTH = 6; const HANDLE_HEIGHT = 20; const HANDLE_TOP_OFFSET = 0; -export default function TimelineScrubber({ - trace, - timestamp, +export default function Scrubber({ + position, onMouseDown, handleTopOffset = HANDLE_TOP_OFFSET, handleWidth = HANDLE_WIDTH, handleHeight = HANDLE_HEIGHT, -}) { - const initialTimestamp = getTraceTimestamp(trace); - const totalDuration = getTraceDuration(trace); - const xPercentage = getPercentageOfInterval(timestamp, initialTimestamp, totalDuration); - +}: ScrubberProps) { + const xPercent = `${position * 100}%`; return ( - + - + ); } - -TimelineScrubber.propTypes = { - onMouseDown: PropTypes.func, - trace: PropTypes.object, - timestamp: PropTypes.number.isRequired, - handleTopOffset: PropTypes.number, - handleWidth: PropTypes.number, - handleHeight: PropTypes.number, -}; diff --git a/src/components/TracePage/TimelineScrubber.test.js b/src/components/TracePage/SpanGraph/Scrubber.test.js similarity index 78% rename from src/components/TracePage/TimelineScrubber.test.js rename to src/components/TracePage/SpanGraph/Scrubber.test.js index fef202c9cb..8711ac97d1 100644 --- a/src/components/TracePage/TimelineScrubber.test.js +++ b/src/components/TracePage/SpanGraph/Scrubber.test.js @@ -22,23 +22,18 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import TimelineScrubber from '../../../src/components/TracePage/TimelineScrubber'; -import traceGenerator from '../../../src/demo/trace-generators'; +import Scrubber from './Scrubber'; -import { getTraceTimestamp, getTraceDuration } from '../../../src/selectors/trace'; - -describe('', () => { - const generatedTrace = traceGenerator.trace({ numberOfSpans: 45 }); +describe('', () => { const defaultProps = { onMouseDown: sinon.spy(), - trace: generatedTrace, - timestamp: getTraceTimestamp(generatedTrace), + position: 0, }; let wrapper; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('contains the proper svg components', () => { @@ -56,8 +51,7 @@ describe('', () => { }); it('calculates the correct x% for a timestamp', () => { - const timestamp = getTraceDuration(generatedTrace) * 0.5 + getTraceTimestamp(generatedTrace); - wrapper = shallow(); + wrapper = shallow(); const line = wrapper.find('line').first(); const rect = wrapper.find('rect').first(); expect(line.prop('x1')).toBe('50%'); diff --git a/src/components/TracePage/SpanGraph/TickLabels.css b/src/components/TracePage/SpanGraph/TickLabels.css new file mode 100644 index 0000000000..5e0c9a20ac --- /dev/null +++ b/src/components/TracePage/SpanGraph/TickLabels.css @@ -0,0 +1,33 @@ +/* +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. +*/ + +.TickLabels { + height: 1.25rem; + position: relative; +} + +.TickLabels--label { + color: #717171; + font-size: 0.8rem; + position: absolute; + user-select: none; +} diff --git a/src/components/TracePage/SpanGraph/SpanGraphTickHeader.js b/src/components/TracePage/SpanGraph/TickLabels.js similarity index 81% rename from src/components/TracePage/SpanGraph/SpanGraphTickHeader.js rename to src/components/TracePage/SpanGraph/TickLabels.js index 906f0b05a8..a00070471f 100644 --- a/src/components/TracePage/SpanGraph/SpanGraphTickHeader.js +++ b/src/components/TracePage/SpanGraph/TickLabels.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,12 +20,18 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import PropTypes from 'prop-types'; import React from 'react'; import { formatDuration } from '../../../utils/date'; -export default function SpanGraphTickHeader(props) { +import './TickLabels.css'; + +type TickLabelsProps = { + numTicks: number, + duration: number, +}; + +export default function TickLabels(props: TickLabelsProps) { const { numTicks, duration } = props; const ticks = []; @@ -31,20 +39,15 @@ export default function SpanGraphTickHeader(props) { const portion = i / numTicks; const style = portion === 1 ? { right: '0%' } : { left: `${portion * 100}%` }; ticks.push( -
+
{formatDuration(duration * portion)}
); } return ( -
+
{ticks}
); } - -SpanGraphTickHeader.propTypes = { - numTicks: PropTypes.number.isRequired, - duration: PropTypes.number.isRequired, -}; diff --git a/src/components/TracePage/SpanGraph/SpanGraphTickHeader.test.js b/src/components/TracePage/SpanGraph/TickLabels.test.js similarity index 88% rename from src/components/TracePage/SpanGraph/SpanGraphTickHeader.test.js rename to src/components/TracePage/SpanGraph/TickLabels.test.js index 1821f54731..62cf0b6fa8 100644 --- a/src/components/TracePage/SpanGraph/SpanGraphTickHeader.test.js +++ b/src/components/TracePage/SpanGraph/TickLabels.test.js @@ -21,9 +21,9 @@ import React from 'react'; import { shallow } from 'enzyme'; -import SpanGraphTickHeader from './SpanGraphTickHeader'; +import TickLabels from './TickLabels'; -describe('', () => { +describe('', () => { const defaultProps = { numTicks: 4, duration: 5000, @@ -33,7 +33,7 @@ describe('', () => { let ticks; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(); ticks = wrapper.find('[data-test="tick"]'); }); @@ -60,6 +60,6 @@ describe('', () => { }); it("doesn't explode if no trace is present", () => { - expect(() => shallow()).not.toThrow(); + expect(() => shallow()).not.toThrow(); }); }); diff --git a/src/components/TracePage/SpanGraph/index.css b/src/components/TracePage/SpanGraph/index.css index 5210423317..77a28a37b9 100644 --- a/src/components/TracePage/SpanGraph/index.css +++ b/src/components/TracePage/SpanGraph/index.css @@ -20,19 +20,24 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -.span-graph--tick { - stroke: #aaa; - stroke-width: 1px; +.SpanGraph--zlayer { + position: relative; + z-index: 1; } -.span-graph--tick-header { +.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%; } -.span-graph--tick-header__label { - position: absolute; - top: 0; - width: auto; - user-select: none; - cursor: default; +.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 4b99908c6a..cae22b935f 100644 --- a/src/components/TracePage/SpanGraph/index.js +++ b/src/components/TracePage/SpanGraph/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,65 +20,217 @@ // 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 React from 'react'; +import { window } from 'global'; -import colorGenerator from '../../../utils/color-generator'; +import GraphTicks from './GraphTicks'; +import CanvasSpanGraph from './CanvasSpanGraph'; +import TickLabels from './TickLabels'; +import Scrubber from './Scrubber'; +import type { Trace } from '../../../types'; import './index.css'; -const MIN_SPAN_WIDTH = 0.002; +const TIMELINE_TICK_INTERVAL = 4; + +type SpanGraphProps = { + height: number, + trace: Trace, + viewRange: [number, number], +}; + +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; -export default function SpanGraph(props) { - const { valueWidth: totalValueWidth, numTicks, items } = props; + static defaultProps = { + height: 60, + }; - const itemHeight = 1 / items.length * 100; + static contextTypes = { + updateTimeRangeFilter: PropTypes.func.isRequired, + }; - const ticks = []; - // i starts at 1, limit is `i < numTicks` so the first and last ticks aren't drawn - for (let i = 1; i < numTicks; i++) { - const x = `${i / numTicks * 100}%`; - ticks.push(); + 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; } - const spanItems = items.map((item, i) => { - const { valueWidth, valueOffset, serviceName } = item; - const key = `span-graph-${i}`; - const fill = colorGenerator.getColorByKey(serviceName); - const width = `${Math.max(valueWidth / totalValueWidth, MIN_SPAN_WIDTH) * 100}%`; + 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 ); - }); - - return ( - - - - {spanItems} - - - ); -} + } -SpanGraph.propTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - valueWidth: PropTypes.number.isRequired, - valueOffset: PropTypes.number.isRequired, - serviceName: PropTypes.string.isRequired, - }) - ).isRequired, - numTicks: PropTypes.number.isRequired, - valueWidth: PropTypes.number.isRequired, -}; + _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)} + /> + } + +
+
+
+ ); + } +} diff --git a/src/components/TracePage/SpanGraph/index.test.js b/src/components/TracePage/SpanGraph/index.test.js index 932c98d7d7..b64ce19940 100644 --- a/src/components/TracePage/SpanGraph/index.test.js +++ b/src/components/TracePage/SpanGraph/index.test.js @@ -19,48 +19,217 @@ // THE SOFTWARE. import React from 'react'; +import sinon from 'sinon'; import { shallow } from 'enzyme'; -import SpanGraph from './'; +import CanvasSpanGraph from './CanvasSpanGraph'; +import SpanGraph from './index'; +import GraphTicks from './GraphTicks'; +import TickLabels from './TickLabels'; +import TimelineScrubber from './Scrubber'; +import traceGenerator from '../../../../src/demo/trace-generators'; +import transformTraceData from '../../../model/transform-trace-data'; +import { polyfill as polyfillAnimationFrame } from '../../../utils/test/requestAnimationFrame'; describe('', () => { - const defaultProps = { - items: [ - { valueWidth: 100, valueOffset: 25, serviceName: 'a' }, - { valueWidth: 100, valueOffset: 50, serviceName: 'b' }, - ], - valueWidth: 200, - numTicks: 4, + polyfillAnimationFrame(window); + + const trace = transformTraceData(traceGenerator.trace({})); + const props = { trace, viewRange: [0, 1] }; + const options = { + context: { + updateTimeRangeFilter: () => {}, + }, }; - let itemsG; - let ticksG; + let wrapper; beforeEach(() => { - const wrapper = shallow(); - itemsG = wrapper.find('[data-test="span-items"]'); - ticksG = wrapper.find('[data-test="ticks"]'); + wrapper = shallow(, options); + }); + + it('renders a ', () => { + expect(wrapper.find(CanvasSpanGraph).length).toBe(1); + }); + + it('renders a ', () => { + expect(wrapper.find(TickLabels).length).toBe(1); + }); + + it('returns a
if a trace is not provided', () => { + wrapper = shallow(, options); + expect(wrapper.matchesElement(
)).toBeTruthy(); }); - it('renders a ', () => { - expect(itemsG.length).toBe(1); + it('renders a filtering box if leftBound exists', () => { + const _props = { ...props, viewRange: [0.2, 1] }; + wrapper = shallow(, options); + const leftBox = wrapper.find('.SpanGraph--inactive'); + expect(leftBox.length).toBe(1); + const width = Number(leftBox.prop('width').slice(0, -1)); + const x = leftBox.prop('x'); + expect(Math.round(width)).toBe(20); + expect(x).toBe(0); }); - it('calculates the height of rects based on the number of items', () => { - const rect = itemsG.find('rect').first(); - expect(rect).toBeDefined(); - expect(rect.prop('height')).toBe('50%'); + it('renders a filtering box if rightBound exists', () => { + const _props = { ...props, viewRange: [0, 0.8] }; + wrapper = shallow(, options); + const rightBox = wrapper.find('.SpanGraph--inactive'); + const width = Number(rightBox.prop('width').slice(0, -1)); + const x = Number(rightBox.prop('x').slice(0, -1)); + expect(rightBox.length).toBe(1); + expect(Math.round(width)).toBe(20); + expect(Math.round(x)).toBe(80); }); - it('creates a for ticks', () => { - expect(ticksG.length).toBe(1); + it('renders handles for the timeRangeFilter', () => { + const { viewRange } = props; + let scrubber = ; + expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); + scrubber = ; + expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); }); - it('creates a line for each ticks excluding the first and last', () => { - expect(ticksG.find('line').length).toBe(defaultProps.numTicks - 1); + it('calls startDragging() for the leftBound handle', () => { + const event = { clientX: 50 }; + sinon.stub(wrapper.instance(), '_startDragging'); + wrapper.find('#trace-page-timeline__left-bound-handle').prop('onMouseDown')(event); + expect(wrapper.instance()._startDragging.calledWith('leftBound', event)).toBeTruthy(); + }); + + it('calls startDragging for the rightBound handle', () => { + const event = { clientX: 50 }; + sinon.stub(wrapper.instance(), '_startDragging'); + wrapper.find('#trace-page-timeline__right-bound-handle').prop('onMouseDown')(event); + expect(wrapper.instance()._startDragging.calledWith('rightBound', event)).toBeTruthy(); + }); + + it('passes the number of ticks to render to components', () => { + const tickHeader = wrapper.find(TickLabels); + const graphTicks = wrapper.find(GraphTicks); + expect(tickHeader.prop('numTicks')).toBeGreaterThan(1); + expect(graphTicks.prop('numTicks')).toBeGreaterThan(1); + expect(tickHeader.prop('numTicks')).toBe(graphTicks.prop('numTicks')); + }); + + it('passes items to CanvasSpanGraph', () => { + const canvasGraph = wrapper.find(CanvasSpanGraph).first(); + const items = trace.spans.map(span => ({ + valueOffset: span.relativeStartTime, + valueWidth: span.duration, + serviceName: span.process.serviceName, + })); + expect(canvasGraph.prop('items')).toEqual(items); + }); + + describe('# shouldComponentUpdate()', () => { + it('returns true for new timeRangeFilter', () => { + const state = { ...wrapper.state(), leftBound: Math.random(), rightBound: Math.random() }; + const instance = wrapper.instance(); + expect(instance.shouldComponentUpdate(props, state, options.context)).toBe(true); + }); + + it('returns true for new trace', () => { + const state = wrapper.state(); + const instance = wrapper.instance(); + const trace2 = transformTraceData(traceGenerator.trace({})); + const altProps = { trace: trace2 }; + expect(instance.shouldComponentUpdate(altProps, state, options.context)).toBe(true); + }); + + it('returns true for currentlyDragging', () => { + const state = { ...wrapper.state(), currentlyDragging: !wrapper.state('currentlyDragging') }; + const instance = wrapper.instance(); + expect(instance.shouldComponentUpdate(props, state, options.context)).toBe(true); + }); + + it('returns false, generally', () => { + const state = wrapper.state(); + const instance = wrapper.instance(); + expect(instance.shouldComponentUpdate(props, state, options.context)).toBe(false); + }); + }); + + describe('# onMouseMove()', () => { + it('does nothing if currentlyDragging is false', () => { + const updateTimeRangeFilter = sinon.spy(); + const context = { ...options.context, updateTimeRangeFilter }; + wrapper = shallow(, { ...options, context }); + wrapper.instance()._onMouseMove({ clientX: 45 }); + expect(wrapper.state('prevX')).toBe(null); + expect(updateTimeRangeFilter.called).toBeFalsy(); + }); + + it('stores the clientX on .state', () => { + wrapper.instance()._wrapper = { clientWidth: 100 }; + wrapper.setState({ currentlyDragging: 'leftBound' }); + wrapper.instance()._onMouseMove({ clientX: 45 }); + expect(wrapper.state('prevX')).toBe(45); + }); + + it('updates the timeRangeFilter for the left handle', () => { + const updateTimeRangeFilter = sinon.spy(); + const context = { ...options.context, updateTimeRangeFilter }; + wrapper = shallow(, { ...options, context }); + wrapper.instance()._wrapper = { clientWidth: 100 }; + const [leftBound, rightBound] = props.viewRange; + const state = { ...wrapper.state(), leftBound, rightBound, prevX: 0, currentlyDragging: 'leftBound' }; + wrapper.setState(state); + wrapper.instance()._onMouseMove({ clientX: 45 }); + wrapper.instance()._publishTimeRange(); + expect(updateTimeRangeFilter.calledWith(0.45, 1)).toBeTruthy(); + }); + + it('updates the timeRangeFilter for the right handle', () => { + const updateTimeRangeFilter = sinon.spy(); + const context = { ...options.context, updateTimeRangeFilter }; + wrapper = shallow(, { ...options, context }); + wrapper.instance()._wrapper = { clientWidth: 100 }; + const [leftBound, rightBound] = props.viewRange; + const state = { + ...wrapper.state(), + leftBound, + rightBound, + prevX: 100, + currentlyDragging: 'rightBound', + }; + wrapper.setState(state); + wrapper.instance()._onMouseMove({ clientX: 45 }); + wrapper.instance()._publishTimeRange(); + expect(updateTimeRangeFilter.calledWith(0, 0.45)).toBeTruthy(); + }); + }); + + describe('# _startDragging()', () => { + it('stores the boundName and the prevX in state', () => { + wrapper.instance()._startDragging('leftBound', { clientX: 100 }); + expect(wrapper.state('currentlyDragging')).toBe('leftBound'); + expect(wrapper.state('prevX')).toBe(100); + }); + + it('binds event listeners to the window', () => { + const oldFn = window.addEventListener; + const fn = jest.fn(); + window.addEventListener = fn; + + wrapper.instance()._startDragging('leftBound', { clientX: 100 }); + expect(fn.mock.calls.length).toBe(2); + const eventNames = [fn.mock.calls[0][0], fn.mock.calls[1][0]].sort(); + expect(eventNames).toEqual(['mousemove', 'mouseup']); + window.addEventListener = oldFn; + }); }); - it('creates a rect for each item in the items prop', () => { - expect(itemsG.find('rect').length).toBe(defaultProps.items.length); + describe('# _stopDragging()', () => { + it('clears currentlyDragging and prevX', () => { + const instance = wrapper.instance(); + instance._startDragging('leftBound', { clientX: 100 }); + expect(wrapper.state('currentlyDragging')).toBe('leftBound'); + expect(wrapper.state('prevX')).toBe(100); + instance._stopDragging(); + expect(wrapper.state('currentlyDragging')).toBe(null); + expect(wrapper.state('prevX')).toBe(null); + }); }); }); diff --git a/src/components/TracePage/SpanGraph/render-into-canvas.js b/src/components/TracePage/SpanGraph/render-into-canvas.js new file mode 100644 index 0000000000..3f09c3de19 --- /dev/null +++ b/src/components/TracePage/SpanGraph/render-into-canvas.js @@ -0,0 +1,49 @@ +// @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. + +const CV_WIDTH = 4000; +const MIN_WIDTH = 50; + +export default function renderIntoCanvas( + canvas: HTMLCanvasElement, + items: { valueWidth: number, valueOffset: number, serviceName: string }[], + totalValueWidth: number, + getFillColor: string => string +) { + // eslint-disable-next-line no-param-reassign + canvas.width = CV_WIDTH; + // eslint-disable-next-line no-param-reassign + canvas.height = items.length; + const ctx = canvas.getContext('2d'); + for (let i = 0; i < items.length; i++) { + const { valueWidth, valueOffset, serviceName } = items[i]; + // eslint-disable-next-line no-bitwise + const x = (valueOffset / totalValueWidth * CV_WIDTH) | 0; + // eslint-disable-next-line no-bitwise + let width = (valueWidth / totalValueWidth * CV_WIDTH) | 0; + if (width < MIN_WIDTH) { + width = MIN_WIDTH; + } + ctx.fillStyle = getFillColor(serviceName); + ctx.fillRect(x, i, width, 1); + } +} diff --git a/src/components/TracePage/TraceSpanGraph.js b/src/components/TracePage/TraceSpanGraph.js deleted file mode 100644 index c9bbfd9540..0000000000 --- a/src/components/TracePage/TraceSpanGraph.js +++ /dev/null @@ -1,215 +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. - -import React, { Component } from 'react'; -import { window } from 'global'; -import PropTypes from 'prop-types'; - -import SpanGraph from './SpanGraph'; -import SpanGraphTickHeader from './SpanGraph/SpanGraphTickHeader'; -import TimelineScrubber from './TimelineScrubber'; -import { getPercentageOfInterval } from '../../utils/date'; - -const TIMELINE_TICK_INTERVAL = 4; - -export default class TraceSpanGraph extends Component { - static get propTypes() { - return { - trace: PropTypes.object, - height: PropTypes.number.isRequired, - }; - } - - static get defaultProps() { - return { - height: 60, - }; - } - - static get contextTypes() { - return { - timeRangeFilter: PropTypes.arrayOf(PropTypes.number).isRequired, - updateTimeRangeFilter: PropTypes.func.isRequired, - }; - } - - constructor(props) { - super(props); - this.state = { currentlyDragging: null, prevX: null }; - } - - shouldComponentUpdate( - { trace: newTrace }, - { currentlyDragging: newCurrentlyDragging }, - { timeRangeFilter: newTimeRangeFilter } - ) { - const { trace } = this.props; - const { currentlyDragging } = this.state; - const { timeRangeFilter } = this.context; - const leftBound = timeRangeFilter[0]; - const rightBound = timeRangeFilter[1]; - const newLeftBound = newTimeRangeFilter[0]; - const newRightBound = newTimeRangeFilter[1]; - - return ( - trace.traceID !== newTrace.traceID || - leftBound !== newLeftBound || - rightBound !== newRightBound || - currentlyDragging !== newCurrentlyDragging - ); - } - - startDragging(boundName, { clientX }) { - this.setState({ currentlyDragging: boundName, prevX: clientX }); - - 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.setState({ currentlyDragging: null, prevX: null }); - } - - onMouseMove({ clientX }) { - const { trace } = this.props; - const { prevX, currentlyDragging } = this.state; - const { timeRangeFilter, updateTimeRangeFilter } = this.context; - - if (!currentlyDragging) { - return; - } - - let leftBound = timeRangeFilter[0]; - let rightBound = timeRangeFilter[1]; - - const deltaX = (clientX - prevX) / this.svg.clientWidth; - const prevValue = { leftBound, rightBound }[currentlyDragging]; - const newValue = prevValue + trace.duration * deltaX; - - // enforce the edges of the graph - switch (currentlyDragging) { - case 'leftBound': - leftBound = Math.max(trace.startTime, newValue); - break; - case 'rightBound': - rightBound = Math.min(trace.endTime, newValue); - break; - /* istanbul ignore next */ default: - break; - } - - this.setState({ prevX: clientX }); - if (leftBound <= rightBound) { - updateTimeRangeFilter(leftBound, rightBound); - } - } - - render() { - const { trace, height } = this.props; - const { currentlyDragging } = this.state; - const { timeRangeFilter } = this.context; - const leftBound = timeRangeFilter[0]; - const rightBound = timeRangeFilter[1]; - - if (!trace) { - return
; - } - - let leftInactive; - if (leftBound) { - leftInactive = getPercentageOfInterval(leftBound, trace.startTime, trace.duration); - } - - let rightInactive; - if (rightBound) { - rightInactive = 100 - getPercentageOfInterval(rightBound, trace.startTime, trace.duration); - } - - return ( -
-
- -
-
- { - this.svg = c; - }} - > - {leftInactive > 0 && - } - {rightInactive > 0 && - } - ({ - valueOffset: span.relativeStartTime, - valueWidth: span.duration, - serviceName: span.process.serviceName, - }))} - /> - {leftBound && - this.startDragging('leftBound', ...args)} - />} - {rightBound && - this.startDragging('rightBound', ...args)} - />} - -
-
- ); - } -} diff --git a/src/components/TracePage/TraceSpanGraph.test.js b/src/components/TracePage/TraceSpanGraph.test.js deleted file mode 100644 index 9c9c8e1450..0000000000 --- a/src/components/TracePage/TraceSpanGraph.test.js +++ /dev/null @@ -1,242 +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. - -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; - -import SpanGraph from './SpanGraph'; -import TraceSpanGraph from './TraceSpanGraph'; -import SpanGraphTickHeader from './SpanGraph/SpanGraphTickHeader'; -import TimelineScrubber from './TimelineScrubber'; -import traceGenerator from '../../../src/demo/trace-generators'; -import transformTraceData from '../../model/transform-trace-data'; - -describe('', () => { - const trace = transformTraceData(traceGenerator.trace({})); - const props = { trace }; - const options = { - context: { - timeRangeFilter: [trace.startTime, trace.startTime + trace.duration], - updateTimeRangeFilter: () => {}, - }, - }; - - let wrapper; - - beforeEach(() => { - wrapper = shallow(, options); - }); - - it('renders a ', () => { - expect(wrapper.find(SpanGraph).length).toBe(1); - }); - - it('renders a ', () => { - expect(wrapper.find(SpanGraphTickHeader).length).toBe(1); - }); - - it('returns a
if a trace is not provided', () => { - wrapper = shallow(, options); - expect(wrapper.matchesElement(
)).toBeTruthy(); - }); - - it('renders a filtering box if leftBound exists', () => { - const context = { - ...options.context, - timeRangeFilter: [trace.startTime + 0.2 * trace.duration, trace.startTime + trace.duration], - }; - wrapper = shallow(, { ...options, context }); - const leftBox = wrapper.find('.trace-page-timeline__graph--inactive'); - expect(leftBox.length).toBe(1); - const width = Number(leftBox.prop('width').slice(0, -1)); - const x = leftBox.prop('x'); - expect(Math.round(width)).toBe(20); - expect(x).toBe(0); - }); - - it('renders a filtering box if rightBound exists', () => { - const context = { - ...options.context, - timeRangeFilter: [trace.startTime, trace.startTime + 0.8 * trace.duration], - }; - wrapper = shallow(, { ...options, context }); - const rightBox = wrapper.find('.trace-page-timeline__graph--inactive'); - const width = Number(rightBox.prop('width').slice(0, -1)); - const x = Number(rightBox.prop('x').slice(0, -1)); - expect(rightBox.length).toBe(1); - expect(Math.round(width)).toBe(20); - expect(Math.round(x)).toBe(80); - }); - - it('renders handles for the timeRangeFilter', () => { - const timeRangeFilter = options.context.timeRangeFilter; - let scrubber = ; - expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); - scrubber = ; - expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); - }); - - it('calls startDragging() for the leftBound handle', () => { - const event = { clientX: 50 }; - sinon.stub(wrapper.instance(), 'startDragging'); - wrapper.find('#trace-page-timeline__left-bound-handle').prop('onMouseDown')(event); - expect(wrapper.instance().startDragging.calledWith('leftBound', event)).toBeTruthy(); - }); - - it('calls startDragging for the rightBound handle', () => { - const event = { clientX: 50 }; - sinon.stub(wrapper.instance(), 'startDragging'); - wrapper.find('#trace-page-timeline__right-bound-handle').prop('onMouseDown')(event); - expect(wrapper.instance().startDragging.calledWith('rightBound', event)).toBeTruthy(); - }); - - it('renders without handles if not filtering', () => { - const context = { ...options.context, timeRangeFilter: [] }; - wrapper = shallow(, { ...options, context }); - expect(wrapper.find('rect').length).toBe(0); - expect(wrapper.find(TimelineScrubber).length).toBe(0); - }); - - it('passes the number of ticks to rendered to components', () => { - const tickHeader = wrapper.find(SpanGraphTickHeader); - const spanGraph = wrapper.find(SpanGraph); - expect(tickHeader.prop('numTicks')).toBeGreaterThan(1); - expect(spanGraph.prop('numTicks')).toBeGreaterThan(1); - expect(tickHeader.prop('numTicks')).toBe(spanGraph.prop('numTicks')); - }); - - it('passes items to SpanGraph', () => { - const spanGraph = wrapper.find(SpanGraph).first(); - const items = trace.spans.map(span => ({ - valueOffset: span.relativeStartTime, - valueWidth: span.duration, - serviceName: span.process.serviceName, - })); - expect(spanGraph.prop('items')).toEqual(items); - }); - - describe('# shouldComponentUpdate()', () => { - it('returns true for new timeRangeFilter', () => { - const state = wrapper.state(); - const context = { timeRangeFilter: [Math.random(), Math.random()] }; - const instance = wrapper.instance(); - expect(instance.shouldComponentUpdate(props, state, context)).toBe(true); - }); - - it('returns true for new trace', () => { - const state = wrapper.state(); - const instance = wrapper.instance(); - const trace2 = transformTraceData(traceGenerator.trace({})); - const altProps = { trace: trace2 }; - expect(instance.shouldComponentUpdate(altProps, state, options.context)).toBe(true); - }); - - it('returns true for currentlyDragging', () => { - const state = { ...wrapper.state(), currentlyDragging: !wrapper.state('currentlyDragging') }; - const instance = wrapper.instance(); - expect(instance.shouldComponentUpdate(props, state, options.context)).toBe(true); - }); - - it('returns false, generally', () => { - const state = wrapper.state(); - const instance = wrapper.instance(); - expect(instance.shouldComponentUpdate(props, state, options.context)).toBe(false); - }); - }); - - describe('# onMouseMove()', () => { - it('does nothing if currentlyDragging is false', () => { - const updateTimeRangeFilter = sinon.spy(); - const context = { ...options.context, updateTimeRangeFilter }; - wrapper = shallow(, { ...options, context }); - wrapper.instance().svg = { clientWidth: 100 }; - wrapper.setState({ currentlyDragging: null }); - wrapper.instance().onMouseMove({ clientX: 45 }); - expect(wrapper.state('prevX')).toBe(null); - expect(updateTimeRangeFilter.called).toBeFalsy(); - }); - - it('stores the clientX on .state', () => { - wrapper.instance().svg = { clientWidth: 100 }; - wrapper.setState({ currentlyDragging: 'leftBound' }); - wrapper.instance().onMouseMove({ clientX: 45 }); - expect(wrapper.state('prevX')).toBe(45); - }); - - it('updates the timeRangeFilter for the left handle', () => { - const timestamp = trace.startTime; - const duration = trace.duration; - const updateTimeRangeFilter = sinon.spy(); - const context = { ...options.context, updateTimeRangeFilter }; - wrapper = shallow(, { ...options, context }); - wrapper.instance().svg = { clientWidth: 100 }; - wrapper.setState({ prevX: 0, currentlyDragging: 'leftBound' }); - wrapper.instance().onMouseMove({ clientX: 45 }); - expect( - updateTimeRangeFilter.calledWith(timestamp + 0.45 * duration, timestamp + duration) - ).toBeTruthy(); - }); - - it('updates the timeRangeFilter for the right handle', () => { - const timestamp = trace.startTime; - const duration = trace.duration; - const updateTimeRangeFilter = sinon.spy(); - const context = { ...options.context, updateTimeRangeFilter }; - wrapper = shallow(, { ...options, context }); - wrapper.instance().svg = { clientWidth: 100 }; - wrapper.setState({ prevX: 100, currentlyDragging: 'rightBound' }); - wrapper.instance().onMouseMove({ clientX: 45 }); - expect(updateTimeRangeFilter.calledWith(timestamp, timestamp + 0.45 * duration)).toBeTruthy(); - }); - }); - - describe('# startDragging()', () => { - it('stores the boundName and the prevX in state', () => { - wrapper.instance().startDragging('leftBound', { clientX: 100 }); - expect(wrapper.state('currentlyDragging')).toBe('leftBound'); - expect(wrapper.state('prevX')).toBe(100); - }); - - it('binds event listeners to the window', () => { - const oldFn = window.addEventListener; - const fn = jest.fn(); - window.addEventListener = fn; - - wrapper.instance().startDragging('leftBound', { clientX: 100 }); - expect(fn.mock.calls.length).toBe(2); - const eventNames = [fn.mock.calls[0][0], fn.mock.calls[1][0]].sort(); - expect(eventNames).toEqual(['mousemove', 'mouseup']); - window.addEventListener = oldFn; - }); - }); - - describe('# stopDragging()', () => { - it('TraceSpanGraph.stopDragging should clear currentlyDragging and prevX', () => { - const instance = wrapper.instance(); - instance.startDragging('leftBound', { clientX: 100 }); - expect(wrapper.state('currentlyDragging')).toBe('leftBound'); - expect(wrapper.state('prevX')).toBe(100); - instance.stopDragging(); - expect(wrapper.state('currentlyDragging')).toBe(null); - expect(wrapper.state('prevX')).toBe(null); - }); - }); -}); diff --git a/src/components/TracePage/TraceTimelineViewer/Ticks.css b/src/components/TracePage/TraceTimelineViewer/Ticks.css index f89b9ac975..71fde1738d 100644 --- a/src/components/TracePage/TraceTimelineViewer/Ticks.css +++ b/src/components/TracePage/TraceTimelineViewer/Ticks.css @@ -27,8 +27,9 @@ THE SOFTWARE. background: #d3d3d3; } .span-row-tick-label { - position: absolute; + bottom: 0.5rem; left: 5px; + position: absolute; } .span-row-tick-label.is-end-anchor { diff --git a/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js b/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js index 484a1ed1f5..152f9076a7 100644 --- a/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js +++ b/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js @@ -347,11 +347,7 @@ class VirtualizedTraceView extends React.PureComponentService & Operation - (tick > 0 ? formatDuration(getDuationAtTick(tick)) : ''))} - ticks={ticks} - /> -

Timeline

+ formatDuration(getDuationAtTick(tick)))} ticks={ticks} />
diff --git a/src/components/TracePage/TraceTimelineViewer/index.js b/src/components/TracePage/TraceTimelineViewer/index.js index 48c8d870fa..3ea28a3b87 100644 --- a/src/components/TracePage/TraceTimelineViewer/index.js +++ b/src/components/TracePage/TraceTimelineViewer/index.js @@ -22,7 +22,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import VirtualizedTraceView from './VirtualizedTraceView'; -import { getPositionInRange } from './utils'; import './grid.css'; import './index.css'; @@ -37,14 +36,13 @@ export default class TraceTimelineViewer extends Component { render() { const { timeRangeFilter: zoomRange, textFilter, trace } = this.props; - const { startTime, endTime } = trace; return (
); diff --git a/src/components/TracePage/TraceTimelineViewer/utils.js b/src/components/TracePage/TraceTimelineViewer/utils.js index 7420efd00a..d15b128b43 100644 --- a/src/components/TracePage/TraceTimelineViewer/utils.js +++ b/src/components/TracePage/TraceTimelineViewer/utils.js @@ -44,24 +44,6 @@ export function getViewedBounds({ min, max, start, end, viewStart, viewEnd }) { }; } -/** - * Given `start` and `end`, returns the position of `value` within that range - * with `0` returned when `value` is equal to `start` and `1` return when it - * is equal to `end`. - * - * @param {number} start The start of the range to find `value`'s position in. - * @param {number} end The end of the range. - * @param {number} value The value to find the position of. - * @return {number} A number representing the placement of `value` - * relative to `start` and `end`. - */ -export function getPositionInRange(start, end, value) { - if (value == null) { - return undefined; - } - return (value - start) / (end - start); -} - /** * Returns `true` if the `span` has a tag matching `key` = `value`. * diff --git a/src/components/TracePage/TraceTimelineViewer/utils.test.js b/src/components/TracePage/TraceTimelineViewer/utils.test.js index 661dfca0f2..d2d1210a94 100644 --- a/src/components/TracePage/TraceTimelineViewer/utils.test.js +++ b/src/components/TracePage/TraceTimelineViewer/utils.test.js @@ -19,7 +19,6 @@ // THE SOFTWARE. import { - getPositionInRange, getViewedBounds, isClientSpan, isErrorSpan, @@ -61,16 +60,6 @@ describe('TraceTimelineViewer/utils', () => { }); }); - describe('getPositionInRange()', () => { - it('gets the position of a value within a range', () => { - expect(getPositionInRange(100, 200, 150)).toBe(0.5); - expect(getPositionInRange(100, 200, 0)).toBe(-1); - expect(getPositionInRange(100, 200, 200)).toBe(1); - expect(getPositionInRange(100, 200, 100)).toBe(0); - expect(getPositionInRange(0, 200, 100)).toBe(0.5); - }); - }); - describe('spanHasTag() and variants', () => { it('returns true iff the key/value pair is found', () => { const tags = traceGenerator.tags(); diff --git a/src/components/TracePage/index.css b/src/components/TracePage/index.css index 3f1ddd3ba9..018a9241f0 100644 --- a/src/components/TracePage/index.css +++ b/src/components/TracePage/index.css @@ -38,60 +38,3 @@ THE SOFTWARE. .trace-timeline-section { border-top: 1px solid #999; } - -/* timeline */ -.trace-page-timeline--tick-container{ - position: relative; - height: 1.25rem; -} - -.trace-page-timeline--tick-container .span-graph--tick-header__label { - font-size: 0.8rem; - color: #717171; -} - -.trace-page-timeline__graph { - background: #f8f8f8; - border: 1px solid #999; - overflow: visible !important; - transform-origin: 0 0; - width: 100%; -} - -.trace-page-timeline__graph.is-dragging { - cursor: ew-resize; -} - -.trace-page-timeline__graph--inactive { - fill: #d6d6d5; -} - -.timeline-scrubber { - cursor: ew-resize; -} - -.timeline-scrubber__line { - stroke: #999; - stroke-width: 1; -} - -.timeline-scrubber:hover .timeline-scrubber__line { - stroke: #777; -} - -.timeline-scrubber__handle { - stroke: #999; - fill: #fff; -} - -.timeline-scrubber:hover .timeline-scrubber__handle { - stroke: #777; -} - -.timeline-scrubber__handle--grip { - r: 2; - fill: #bbb; -} -.timeline-scrubber:hover .timeline-scrubber__handle--grip { - fill: #999; -} diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index 59d76e585c..035727f37a 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -26,12 +26,11 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import TracePageHeader from './TracePageHeader'; -import TraceSpanGraph from './TraceSpanGraph'; +import SpanGraph from './SpanGraph'; import TraceTimelineViewer from './TraceTimelineViewer'; import NotFound from '../App/NotFound'; import * as jaegerApiActions from '../../actions/jaeger-api'; import { getTraceName } from '../../model/trace-viewer'; -import { getTraceTimestamp, getTraceEndTimestamp, getTraceId } from '../../selectors/trace'; import colorGenerator from '../../utils/color-generator'; import './index.css'; @@ -49,7 +48,6 @@ export default class TracePage extends Component { static get childContextTypes() { return { textFilter: PropTypes.string, - timeRangeFilter: PropTypes.arrayOf(PropTypes.number), updateTextFilter: PropTypes.func, updateTimeRangeFilter: PropTypes.func, slimView: PropTypes.bool, @@ -72,6 +70,7 @@ export default class TracePage extends Component { getChildContext() { const state = { ...this.state }; delete state.headerHeight; + delete state.timeRangeFilter; return { updateTextFilter: this.updateTextFilter.bind(this), updateTimeRangeFilter: this.updateTimeRangeFilter.bind(this), @@ -92,7 +91,7 @@ export default class TracePage extends Component { this.ensureTraceFetched(); return; } - if (!(trace instanceof Error) && (!prevTrace || getTraceId(prevTrace) !== getTraceId(trace))) { + if (!(trace instanceof Error) && (!prevTrace || prevTrace.traceID !== trace.traceID)) { this.setDefaultTimeRange(); } } @@ -114,7 +113,7 @@ export default class TracePage extends Component { this.updateTimeRangeFilter(null, null); return; } - this.updateTimeRangeFilter(getTraceTimestamp(trace), getTraceEndTimestamp(trace)); + this.updateTimeRangeFilter(0, 1); } updateTextFilter(textFilter) { @@ -166,7 +165,7 @@ export default class TracePage extends Component { traceID={traceID} onSlimViewClicked={this.toggleSlimView} /> - {!slimView && } + {!slimView && } {headerHeight &&
diff --git a/src/components/TracePage/index.test.js b/src/components/TracePage/index.test.js index 737a4d93c4..1e9fe7a101 100644 --- a/src/components/TracePage/index.test.js +++ b/src/components/TracePage/index.test.js @@ -25,7 +25,7 @@ import { shallow, mount } from 'enzyme'; import traceGenerator from '../../demo/trace-generators'; import TracePage from './'; import TracePageHeader from './TracePageHeader'; -import TraceSpanGraph from './TraceSpanGraph'; +import SpanGraph from './SpanGraph'; import transformTraceData from '../../model/transform-trace-data'; describe('', () => { @@ -46,9 +46,8 @@ describe('', () => { expect(wrapper.find(TracePageHeader).get(0)).toBeTruthy(); }); - it('renders a ', () => { - const props = { trace: defaultProps.trace }; - expect(wrapper.contains()).toBeTruthy(); + it('renders a ', () => { + expect(wrapper.find(SpanGraph).length).toBe(1); }); it('renders an empty page when not provided a trace', () => { diff --git a/src/utils/date.js b/src/utils/date.js index b38be0b5c1..55b8385f64 100644 --- a/src/utils/date.js +++ b/src/utils/date.js @@ -40,17 +40,6 @@ export function getPercentageOfDuration(duration, totalDuration) { return duration / totalDuration * 100; } -/** - * @param {number} timestamp - * @param {number} initialTimestamp - * @param {number} totalDuration - * @return {number} 0-100 percentage value for location of timestamp in interval starting - * at initialTimestamp and lasting totalDuration - */ -export function getPercentageOfInterval(timestamp, initialTimestamp, totalDuration) { - return getPercentageOfDuration(timestamp - initialTimestamp, totalDuration); -} - const quantizeDuration = (duration, floatPrecision, conversionFactor) => toFloatPrecision(duration / conversionFactor, floatPrecision) * conversionFactor;