From fe35bbd8820edc2164dc5230f0ba7255f2dc99b9 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 3 Aug 2017 18:06:29 -0400 Subject: [PATCH 01/20] Update prettier and wrap at 110 instead of 80 --- .../TraceTimelineViewer/SpanBreakdownGraph.js | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanBreakdownGraph.js diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBreakdownGraph.js b/src/components/TracePage/TraceTimelineViewer/SpanBreakdownGraph.js new file mode 100644 index 0000000000..4f21738869 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/SpanBreakdownGraph.js @@ -0,0 +1,87 @@ +// 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 PropTypes from 'prop-types'; +import React from 'react'; +import SpanGraphTick from '../../SpanGraph/SpanGraphTick'; +import logPropTypes from '../../../propTypes/log'; + +export default function SpanBreakdownBar({ span }) { + const padding = 3; + const fontSize = 11; + return ( +
+
+ + 0 ms + + + {span.duration} ms + +
+ + {span.logs.map((log, i) => + + )} + +
+ ); +} + +SpanBreakdownBar.propTypes = { + span: PropTypes.shape({ + duration: PropTypes.number, + timestamp: PropTypes.number, + logs: PropTypes.arrayOf(logPropTypes), + }), +}; From acbfdee0bbd27b056ded19fea21a7edfeb631b6f Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Fri, 11 Aug 2017 00:59:09 -0400 Subject: [PATCH 02/20] WIP commit for refactoring trace detail view TODO - Test are currently in a bad state - Finish shifting to transformed trace, exclusively, instead of raw trace - Misc cleanup (unused / ordering of vars, imports, etc) is outstanding while changes are WIP - Continued refinement of trace-detail components, selectors and utils - Many styles will be moved to CSS - Will likely remove `model/trace-viewer.js` SpanGraph - Fix #49 - Span position in graph doesn not match its position in the detail - Ticks in span graph made to match trace detail (in number and formatting) - Span graph refactored to trim down files and DOM elements TracePageHeader - `trace` prop removed - Added props for various title values instead of deriving them from `trace` Trace Detail - Several components split out into separate files - `transformTrace` to use alread span tree to determine span depth Span Detail - Fix uber/jaeger #326: extraneous scrollbars in trace views - Fix: Some tags were not being rendered due to clashing keys (observed in a log message) - Tall content scrolls via entire table instead of single table cell - Horizontal scrolling for wide content (e.g. long log values) - Full width of the header is clickable for tags, process, and logs headers (instead of header text, only) - Service and endpoint are shown on mouseover anywhere in span bar row Misc - Several TraceTimelineViewer / utils removed - `TreeNode` `.walk()` method can now be used to calculate the depth, avoiding use of less efficient `.getPath()` - Removed several `console.error` warnings caused by React key issues --- src/components/SpanGraph/SpanGraphSpan.js | 108 --------- .../SpanGraph/SpanGraphSpan.test.js | 145 ------------- src/components/SpanGraph/SpanGraphTick.js | 75 ------- .../SpanGraph/SpanGraphTick.test.js | 139 ------------ .../SpanGraph/SpanGraphTickHeader.js | 70 ------ src/components/SpanGraph/index.js | 120 ---------- .../{ => TracePage}/SpanGraph/SpanGraph.css | 3 +- .../SpanGraph/SpanGraphTickHeader.js | 50 +++++ .../SpanGraph/SpanGraphTickHeader.test.js | 0 src/components/TracePage/SpanGraph/index.js | 81 +++++++ .../{ => TracePage}/SpanGraph/index.test.js | 2 +- src/components/TracePage/TracePage.css | 10 +- src/components/TracePage/TracePageHeader.js | 91 ++++---- ...TracePageTimeline.js => TraceSpanGraph.js} | 46 ++-- ...imeline.test.js => TraceSpanGraph.test.js} | 118 +++++----- .../TracePage/TraceTimelineViewer/SpanBar.js | 118 ++++++++++ .../TraceTimelineViewer/SpanBreakdownGraph.js | 87 -------- .../TraceTimelineViewer/SpanDetail.js | 90 ++++---- .../TracePage/TraceTimelineViewer/Ticks.js | 61 ++++++ .../TraceTimelineViewer/TimelineRow.js} | 72 +++--- .../TracePage/TraceTimelineViewer/index.js | 205 +++--------------- .../TraceTimelineViewer/transforms.js | 30 ++- .../TracePage/TraceTimelineViewer/utils.js | 49 +++-- src/components/TracePage/index.js | 36 ++- src/components/TracePage/index.test.js | 6 +- src/index.js | 2 +- .../trace-viewer.js} | 21 +- src/utils/TreeNode.js | 10 +- 28 files changed, 630 insertions(+), 1215 deletions(-) delete mode 100644 src/components/SpanGraph/SpanGraphSpan.js delete mode 100644 src/components/SpanGraph/SpanGraphSpan.test.js delete mode 100644 src/components/SpanGraph/SpanGraphTick.js delete mode 100644 src/components/SpanGraph/SpanGraphTick.test.js delete mode 100644 src/components/SpanGraph/SpanGraphTickHeader.js delete mode 100644 src/components/SpanGraph/index.js rename src/components/{ => TracePage}/SpanGraph/SpanGraph.css (84%) create mode 100644 src/components/TracePage/SpanGraph/SpanGraphTickHeader.js rename src/components/{ => TracePage}/SpanGraph/SpanGraphTickHeader.test.js (100%) create mode 100644 src/components/TracePage/SpanGraph/index.js rename src/components/{ => TracePage}/SpanGraph/index.test.js (98%) rename src/components/TracePage/{TracePageTimeline.js => TraceSpanGraph.js} (87%) rename src/components/TracePage/{TracePageTimeline.test.js => TraceSpanGraph.test.js} (66%) create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanBar.js delete mode 100644 src/components/TracePage/TraceTimelineViewer/SpanBreakdownGraph.js create mode 100644 src/components/TracePage/TraceTimelineViewer/Ticks.js rename src/components/{SpanGraph/SpanGraphTickHeaderLabel.test.js => TracePage/TraceTimelineViewer/TimelineRow.js} (50%) rename src/{components/SpanGraph/SpanGraphTickHeaderLabel.js => model/trace-viewer.js} (67%) diff --git a/src/components/SpanGraph/SpanGraphSpan.js b/src/components/SpanGraph/SpanGraphSpan.js deleted file mode 100644 index df9b6b4212..0000000000 --- a/src/components/SpanGraph/SpanGraphSpan.js +++ /dev/null @@ -1,108 +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 PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import shallowCompare from 'react-addons-shallow-compare'; - -import spanPropTypes from '../../propTypes/span'; -import colorGenerator from '../../utils/color-generator'; -import { getPercentageOfDuration, getPercentageOfInterval } from '../../utils/date'; -import { getSpanTimestamp, getSpanDuration, getSpanServiceName } from '../../selectors/span'; - -export const MIN_SPAN_WIDTH = 0.2; // in percent - -export default class SpanGraphSpan extends Component { - static get propTypes() { - return { - index: PropTypes.number.isRequired, - initialTimestamp: PropTypes.number.isRequired, - rowHeight: PropTypes.number.isRequired, - rowPadding: PropTypes.number.isRequired, - span: spanPropTypes.isRequired, - label: PropTypes.string, - totalDuration: PropTypes.number.isRequired, - onClick: PropTypes.func, - }; - } - - static get defaultProps() { - return { - fill: '#11939A', - }; - } - - shouldComponentUpdate(newProps) { - return shallowCompare(this, newProps); - } - - render() { - const { - index, - initialTimestamp, - label, - rowHeight, - rowPadding, - span, - totalDuration, - ...rest - } = this.props; - const topOffset = index * (rowHeight + rowPadding * 2) + rowPadding * 2; - const spanTimestamp = getSpanTimestamp(span); - const spanDuration = getSpanDuration(span); - const leftOffset = getPercentageOfInterval(spanTimestamp, initialTimestamp, totalDuration); - const width = getPercentageOfDuration(spanDuration, totalDuration); - - // wrap all "onWhatever" handlers to pass the span along as the first argument. - // attach any other props to the spreadable object - const handlerFilter = name => typeof rest[name] === 'function' && name.substr(0, 2) === 'on'; - const handlers = Object.keys(rest).filter(handlerFilter).reduce( - (obj, fnName) => - Object.assign(obj, { - [fnName]: (...args) => rest[fnName](span, ...args), - }), - {} - ); - const spreadable = Object.keys(rest) - .filter(name => !handlerFilter(name)) - .reduce((obj, name) => Object.assign(obj, { [name]: rest[name] }), {}); - return ( - - {label && - - {label} - } - - - ); - } -} diff --git a/src/components/SpanGraph/SpanGraphSpan.test.js b/src/components/SpanGraph/SpanGraphSpan.test.js deleted file mode 100644 index f05e5a5fb4..0000000000 --- a/src/components/SpanGraph/SpanGraphSpan.test.js +++ /dev/null @@ -1,145 +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 { shallow } from 'enzyme'; - -import SpanGraphSpan, { MIN_SPAN_WIDTH } from './SpanGraphSpan'; - -const initialTimestamp = new Date().getTime() * 1000; -const defaultProps = { - initialTimestamp, - totalDuration: 4000000, - rowHeight: 10, - rowPadding: 1, - index: 1, - span: { - spanID: 'span-id', - startTime: initialTimestamp + 1000000, - duration: 1000000, - process: 'Test', - }, -}; - -it(' should render a ', () => { - const wrapper = shallow(); - - expect(wrapper.find('rect').length).toBe(1); -}); - -it(' should use the spanID as the id', () => { - const wrapper = shallow(); - const rect = wrapper.find('rect').first(); - - expect(rect.prop('id')).toBe(defaultProps.span.spanID); -}); - -it(' should have the height set to the rowHeight', () => { - const wrapper = shallow(); - const rect = wrapper.find('rect').first(); - - expect(rect.prop('height')).toBe(defaultProps.rowHeight); -}); - -it(' should have the width based on the duration', () => { - const wrapper = shallow(); - const rect = wrapper.find('rect').first(); - - expect(rect.prop('width')).toBe('25%'); -}); - -it(' should enforce a max width', () => { - const wrapper = shallow( - - ); - - const rect = wrapper.find('rect').first(); - expect(rect.prop('width')).toBe(`${MIN_SPAN_WIDTH}%`); -}); - -it(' should calculate the left offset for the timestamps', () => { - let wrapper; - let rect; - - wrapper = shallow(); - rect = wrapper.find('rect').first(); - expect(rect.prop('x')).toBe('25%'); - - // (4 * (15 + 6)) + 6. - wrapper = shallow( - - ); - rect = wrapper.find('rect').first(); - expect(rect.prop('x')).toBe('50%'); -}); - -it(' should calculate the top offset for the index', () => { - let wrapper; - let rect; - - // (1 * (10 + 2)) + 2. - wrapper = shallow(); - rect = wrapper.find('rect').first(); - expect(rect.prop('y')).toBe(14); - - // (4 * (15 + 6)) + 6. - wrapper = shallow(); - rect = wrapper.find('rect').first(); - expect(rect.prop('y')).toBe(90); - - // (0 * (10 + 2)) + 2. - wrapper = shallow(); - rect = wrapper.find('rect').first(); - expect(rect.prop('y')).toBe(2); -}); - -it(' should decorate handlers with the span', () => { - const event = {}; - - function onClick(span, passedEvent) { - expect(span).toEqual(defaultProps.span); - expect(passedEvent).toEqual(event); - } - - const wrapper = shallow(); - - const rect = wrapper.find('rect').first(); - rect.props().onClick(event); -}); - -it(' should spread unmatched props onto the rect', () => { - const wrapper = shallow(); - - const rect = wrapper.find('rect').first(); - expect(rect.prop('stroke')).toBe('red'); -}); diff --git a/src/components/SpanGraph/SpanGraphTick.js b/src/components/SpanGraph/SpanGraphTick.js deleted file mode 100644 index e41307028f..0000000000 --- a/src/components/SpanGraph/SpanGraphTick.js +++ /dev/null @@ -1,75 +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 PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { isEqual } from 'lodash'; - -import spanGraphTickPropTypes from '../../propTypes/spanGraphTick'; -import { DEFAULT_TICK_WIDTH } from '../../selectors/trace'; -import { getPercentageOfDuration } from '../../utils/date'; - -export default class SpanGraphTick extends Component { - static get propTypes() { - return { - color: PropTypes.string, - initialTimestamp: PropTypes.number.isRequired, - tick: spanGraphTickPropTypes.isRequired, - totalDuration: PropTypes.number.isRequired, - }; - } - - static get defaultProps() { - return { - color: '#E5E5E4', - }; - } - - shouldComponentUpdate(nextProps) { - return !isEqual(this.props, nextProps); - } - - render() { - const { color, initialTimestamp, tick, totalDuration } = this.props; - const { timestamp, width = DEFAULT_TICK_WIDTH, ...rest } = tick; - const timeSinceSpanStart = timestamp - initialTimestamp; - const x = getPercentageOfDuration(timeSinceSpanStart, totalDuration); - - let strokeWidth = width; - if (Math.floor(x) === 0 || Math.ceil(x) === 100) { - strokeWidth = strokeWidth * 2 - 1; - } - - return ( - - ); - } -} diff --git a/src/components/SpanGraph/SpanGraphTick.test.js b/src/components/SpanGraph/SpanGraphTick.test.js deleted file mode 100644 index e0c7f920da..0000000000 --- a/src/components/SpanGraph/SpanGraphTick.test.js +++ /dev/null @@ -1,139 +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 { shallow } from 'enzyme'; - -import SpanGraphTick from './SpanGraphTick'; - -const timestamp = new Date().getTime() * 1000; -const defaultProps = { - index: 0, - initialTimestamp: timestamp, - tick: { timestamp: timestamp + 1000000, width: 3 }, - totalDuration: 4000000, -}; - -it(' should render a ', () => { - const wrapper = shallow(); - - expect(wrapper.find('line').length).toBe(1); -}); - -it(' should draw the line at the height of the container', () => { - const wrapper = shallow(); - const line = wrapper.find('line').first(); - - expect(line.prop('y1')).toBe('0%'); - expect(line.prop('y2')).toBe('100%'); -}); - -it(' should place the x value based on props', () => { - const wrapper = shallow(); - const line = wrapper.find('line').first(); - - // totalDuration is 4 seconds, tick timestamp is 1 second in, - // (tick will be a quarter of the way across) - // container width is 100, so x is 25. - - expect(line.prop('x1')).toBe('25%'); - expect(line.prop('x2')).toBe('25%'); -}); - -it(' should push the increase width if this is the initialTimestamp', () => { - let wrapper; - let line; - - wrapper = shallow( - - ); - line = wrapper.find('line').first(); - expect(line.prop('strokeWidth')).toBe(5); - - wrapper = shallow( - - ); - line = wrapper.find('line').first(); - expect(line.prop('strokeWidth')).toBe(15); -}); - -it(' should push the x value over if this is at the totalDuration', () => { - let wrapper; - let line; - - wrapper = shallow( - - ); - line = wrapper.find('line').first(); - expect(line.prop('strokeWidth')).toBe(5); - - wrapper = shallow( - - ); - line = wrapper.find('line').first(); - expect(line.prop('strokeWidth')).toBe(15); -}); - -it(' should spread unhandled tick properties', () => { - const wrapper = shallow( - - ); - const line = wrapper.find('line').first(); - expect(line.prop('stroke')).toBe('black'); -}); - -it(' should make the width of the tick based on the tick definition', () => { - let wrapper; - let line; - - wrapper = shallow(); - line = wrapper.find('line').first(); - expect(line.prop('strokeWidth')).toBe(3); - - wrapper = shallow(); - line = wrapper.find('line').first(); - expect(line.prop('strokeWidth')).toBe(8); -}); diff --git a/src/components/SpanGraph/SpanGraphTickHeader.js b/src/components/SpanGraph/SpanGraphTickHeader.js deleted file mode 100644 index a0777e902b..0000000000 --- a/src/components/SpanGraph/SpanGraphTickHeader.js +++ /dev/null @@ -1,70 +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 PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { isEqual } from 'lodash'; - -import tracePropTypes from '../../propTypes/trace'; -import spanGraphTickPropTypes from '../../propTypes/spanGraphTick'; -import { getTraceDuration, getTraceTimestamp } from '../../selectors/trace'; -import { getPercentageOfInterval } from '../../utils/date'; - -import SpanGraphTickHeaderLabel from './SpanGraphTickHeaderLabel'; - -export default class SpanGraphTickHeader extends Component { - shouldComponentUpdate({ ticks }) { - const { ticks: prevTicks } = this.props; - return !isEqual(ticks, prevTicks); - } - - render() { - const { ticks, trace } = this.props; - - return ( - trace && -
- {ticks.map(tick => { - const leftOffset = getPercentageOfInterval( - tick.timestamp, - getTraceTimestamp(trace), - getTraceDuration(trace) - ); - - const style = Math.ceil(leftOffset) === 100 ? { right: '0%' } : { left: `${leftOffset}%` }; - - return ( - - ); - })} -
- ); - } -} - -SpanGraphTickHeader.propTypes = { - trace: tracePropTypes, - ticks: PropTypes.arrayOf(spanGraphTickPropTypes).isRequired, -}; diff --git a/src/components/SpanGraph/index.js b/src/components/SpanGraph/index.js deleted file mode 100644 index 6339bd6d97..0000000000 --- a/src/components/SpanGraph/index.js +++ /dev/null @@ -1,120 +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 PropTypes from 'prop-types'; -import React, { Children } from 'react'; - -import './SpanGraph.css'; -import SpanGraphTick from './SpanGraphTick'; -import SpanGraphSpan from './SpanGraphSpan'; -import tracePropTypes from '../../propTypes/trace'; -import spanPropTypes from '../../propTypes/span'; -import spanGraphTickPropTypes from '../../propTypes/spanGraphTick'; -import { getTraceId, getTraceDuration, getTraceTimestamp, getTraceSpans } from '../../selectors/trace'; -import { getSpanId } from '../../selectors/span'; - -export default function SpanGraph({ - trace, - getSpanLabel, - rowHeight, - rowPadding, - ticks, - spans = getTraceSpans(trace), - onSpanClick, - children, - ...rest -}) { - let childElements = children; - - // grab handler functions for spans (such as onSpanClick) off of the - // rest of the props and build an object with them to be applied to the spans - const spanHandlerFilter = name => - typeof rest[name] === 'function' && name.substr(0, 2) === 'on' && name.substr(-4) === 'Span'; - const spreadable = Object.keys(rest) - .filter(name => !spanHandlerFilter(name)) - .reduce((obj, name) => Object.assign(obj, { [name]: rest[name] }), {}); - - if (!childElements) { - const spanHandlers = Object.keys(rest).filter(spanHandlerFilter).reduce( - (obj, fnName) => - Object.assign(obj, { - [fnName.substr(0, fnName.length - 'Span'.length)]: rest[fnName], - }), - {} - ); - - childElements = spans.map((span, idx) => - onSpanClick(span.spanID)} - key={`trace-${getTraceId(trace)}-span-${getSpanId(span)}`} - span={span} - index={idx} - label={getSpanLabel(span)} - initialTimestamp={getTraceTimestamp(trace)} - totalDuration={getTraceDuration(trace)} - rowHeight={rowHeight} - rowPadding={rowPadding} - {...spanHandlers} - /> - ); - } - - const height = (rowHeight + rowPadding * 2) * Children.count(childElements); - - return ( - - {ticks.map((tick, idx) => - - )} - - {childElements} - - ); -} - -SpanGraph.propTypes = { - children: PropTypes.any, - getSpanLabel: PropTypes.func, - rowHeight: PropTypes.number, - rowPadding: PropTypes.number, - ticks: PropTypes.arrayOf(spanGraphTickPropTypes), - trace: tracePropTypes.isRequired, - spans: PropTypes.arrayOf(spanPropTypes), - onSpanClick: PropTypes.func, -}; - -SpanGraph.defaultProps = { - getSpanLabel: () => '', - rowHeight: 12, - rowPadding: 5, - ticks: [], - onSpanClick: noop => noop, -}; diff --git a/src/components/SpanGraph/SpanGraph.css b/src/components/TracePage/SpanGraph/SpanGraph.css similarity index 84% rename from src/components/SpanGraph/SpanGraph.css rename to src/components/TracePage/SpanGraph/SpanGraph.css index 04b6e05eb5..761e51ebff 100644 --- a/src/components/SpanGraph/SpanGraph.css +++ b/src/components/TracePage/SpanGraph/SpanGraph.css @@ -1,6 +1,7 @@ .span-graph--tick { - stroke: #E5E5E4; + stroke: #e5e5e4; opacity: 0.5; + stroke-width: 2px; } .span-graph--tick-header { diff --git a/src/components/TracePage/SpanGraph/SpanGraphTickHeader.js b/src/components/TracePage/SpanGraph/SpanGraphTickHeader.js new file mode 100644 index 0000000000..f94446e2f4 --- /dev/null +++ b/src/components/TracePage/SpanGraph/SpanGraphTickHeader.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 PropTypes from 'prop-types'; +import React from 'react'; + +import { formatDuration } from '../../../utils/date'; + +export default function SpanGraphTickHeader(props) { + const { numTicks, duration } = props; + + const ticks = []; + for (let i = 0; i < numTicks + 1; i++) { + 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/SpanGraph/SpanGraphTickHeader.test.js b/src/components/TracePage/SpanGraph/SpanGraphTickHeader.test.js similarity index 100% rename from src/components/SpanGraph/SpanGraphTickHeader.test.js rename to src/components/TracePage/SpanGraph/SpanGraphTickHeader.test.js diff --git a/src/components/TracePage/SpanGraph/index.js b/src/components/TracePage/SpanGraph/index.js new file mode 100644 index 0000000000..5f7c472eb1 --- /dev/null +++ b/src/components/TracePage/SpanGraph/index.js @@ -0,0 +1,81 @@ +// 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 PropTypes from 'prop-types'; +import React from 'react'; + +import colorGenerator from '../../../utils/color-generator'; + +import './SpanGraph.css'; + +const MIN_SPAN_WIDTH = 0.002; + +export default function SpanGraph(props) { + const { valueWidth: totalValueWidth, numTicks, items } = props; + + const itemHeight = 1 / items.length * 100; + + const ticks = []; + for (let i = 0; i < numTicks + 1; i++) { + const x = `${i / numTicks * 100}%`; + ticks.push(); + } + + 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}%`; + return ( + + ); + }); + + 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, +}; diff --git a/src/components/SpanGraph/index.test.js b/src/components/TracePage/SpanGraph/index.test.js similarity index 98% rename from src/components/SpanGraph/index.test.js rename to src/components/TracePage/SpanGraph/index.test.js index 93d2f45126..a75736d23a 100644 --- a/src/components/SpanGraph/index.test.js +++ b/src/components/TracePage/SpanGraph/index.test.js @@ -25,7 +25,7 @@ import SpanGraph from '.'; import SpanGraphTick from './SpanGraphTick'; import SpanGraphSpan from './SpanGraphSpan'; -import { getTicksForTrace } from '../../selectors/trace'; +import { getTicksForTrace } from '../../../../../selectors/trace'; const initialTimestamp = new Date().getTime() * 1000; const trace = { diff --git a/src/components/TracePage/TracePage.css b/src/components/TracePage/TracePage.css index 7309002047..a30569dda7 100644 --- a/src/components/TracePage/TracePage.css +++ b/src/components/TracePage/TracePage.css @@ -14,7 +14,7 @@ } .trace-page-timeline__graph--inactive { - fill: #D6D6D5; + fill: #d6d6d5; } .timeline-scrubber { @@ -22,15 +22,15 @@ } .timeline-scrubber__line { - stroke: #D6D6D5; + stroke: #d6d6d5; } .timeline-scrubber__handle { - stroke: #E5E5E4; + stroke: #e5e5e4; fill: white; } .timeline-scrubber__handle--grip { r: 2; - fill: #E5E5E4; -} \ No newline at end of file + fill: #e5e5e4; +} diff --git a/src/components/TracePage/TracePageHeader.js b/src/components/TracePage/TracePageHeader.js index ac495ee033..6757ffbf7c 100644 --- a/src/components/TracePage/TracePageHeader.js +++ b/src/components/TracePage/TracePageHeader.js @@ -22,91 +22,70 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Dropdown, Menu } from 'semantic-ui-react'; -import tracePropTypes from '../../propTypes/trace'; -import { formatDatetime } from '../../utils/date'; - -import { - formatDurationForTrace, - getTraceDepth, - getTraceDuration, - getTraceName, - getTraceServiceCount, - getTraceSpanCount, - getTraceTimestamp, -} from '../../selectors/trace'; - -function MoreTraceOptionsDropdown({ traceID }) { - return ( - - - - - - View Trace JSON - - - - - - ); -} - -MoreTraceOptionsDropdown.propTypes = { - traceID: PropTypes.string, -}; +import { formatDatetime, formatDuration } from '../../utils/date'; export const HEADER_ITEMS = [ { key: 'timestamp', title: 'Trace Start', - renderer: trace => formatDatetime(getTraceTimestamp(trace)), + renderer: props => formatDatetime(props.timestampMs * 1000), }, { key: 'duration', title: 'Duration', - renderer: trace => formatDurationForTrace({ trace, duration: getTraceDuration(trace) }), + renderer: props => formatDuration(props.durationMs * 1000), }, { key: 'service-count', title: 'Services', - renderer: trace => getTraceServiceCount(trace), + propName: 'numServices', }, { key: 'depth', title: 'Depth', - renderer: trace => getTraceDepth(trace), + propName: 'maxDepth', }, { key: 'span-count', title: 'Total Spans', - renderer: trace => getTraceSpanCount(trace), + propName: 'numSpans', }, ]; -export default function TracePageHeader( - { trace, slimView = false, onSlimViewClicked = noop => noop }, - { updateTextFilter, textFilter } -) { - if (!trace) { - return
; +export default function TracePageHeader(props, context) { + const { traceID, name, slimView, onSlimViewClicked } = props; + const { updateTextFilter, textFilter } = context; + + if (!traceID) { + return null; } return ( -
+

- onSlimViewClicked()}> + - {getTraceName(trace)} + {name}

- + + + + + + View Trace JSON + + + + +
@@ -115,21 +94,19 @@ export default function TracePageHeader( type="text" defaultValue={textFilter} placeholder="Search..." - onChange={({ target: { value } }) => updateTextFilter(value)} + onChange={event => updateTextFilter(event.target.value)} />
{!slimView &&
- {HEADER_ITEMS.map(({ renderer, title, ...itemProps }) => -
+ {HEADER_ITEMS.map(({ renderer, propName, title, key }) => +
{title}:{' '} - - {renderer(trace)} - + {propName ? props[propName] : renderer(props)}
)}
} @@ -138,7 +115,13 @@ export default function TracePageHeader( } TracePageHeader.propTypes = { - trace: tracePropTypes, + traceID: PropTypes.string, + name: PropTypes.string, + maxDepth: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + numServices: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + numSpans: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + durationMs: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + timestampMs: PropTypes.number, // eslint-disable-line react/no-unused-prop-types slimView: PropTypes.bool, onSlimViewClicked: PropTypes.func, }; diff --git a/src/components/TracePage/TracePageTimeline.js b/src/components/TracePage/TraceSpanGraph.js similarity index 87% rename from src/components/TracePage/TracePageTimeline.js rename to src/components/TracePage/TraceSpanGraph.js index 2105c7ea39..eb1b9ab431 100644 --- a/src/components/TracePage/TracePageTimeline.js +++ b/src/components/TracePage/TraceSpanGraph.js @@ -22,36 +22,28 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { window } from 'global'; -import SpanGraph from '../SpanGraph'; -import SpanGraphTickHeader from '../SpanGraph/SpanGraphTickHeader'; +import SpanGraph from './SpanGraph'; +import SpanGraphTickHeader from './SpanGraph/SpanGraphTickHeader'; import tracePropTypes from '../../propTypes/trace'; import TimelineScrubber from './TimelineScrubber'; import { getTraceId, - getSortedSpans, getTicksForTrace, getTraceTimestamp, getTraceEndTimestamp, getTraceDuration, - getTraceSpans, getTraceSpanCount, } from '../../selectors/trace'; -import { getSpanTimestamp } from '../../selectors/span'; -import { numberSortComparator } from '../../utils/sort'; import { getPercentageOfInterval } from '../../utils/date'; -const TIMELINE_TICK_INTERVAL = 5; +const TIMELINE_TICK_INTERVAL = 4; const TIMELINE_TICK_WIDTH = 2; -const TIMELINE_TRACE_SORT = { - dir: 1, - selector: getSpanTimestamp, - comparator: numberSortComparator, -}; -export default class TracePageTimeline extends Component { +export default class TraceSpanGraph extends Component { static get propTypes() { return { + xformedTrace: PropTypes.object, trace: tracePropTypes, height: PropTypes.number.isRequired, }; @@ -59,7 +51,7 @@ export default class TracePageTimeline extends Component { static get defaultProps() { return { - height: 50, + height: 60, }; } @@ -152,7 +144,7 @@ export default class TracePageTimeline extends Component { } render() { - const { trace, height } = this.props; + const { trace, xformedTrace, height } = this.props; const { currentlyDragging } = this.state; const { timeRangeFilter } = this.context; const leftBound = timeRangeFilter[0]; @@ -162,12 +154,6 @@ export default class TracePageTimeline extends Component { return
; } - const ticks = getTicksForTrace({ - trace, - interval: TIMELINE_TICK_INTERVAL, - width: TIMELINE_TICK_WIDTH, - }); - const initialTimestamp = getTraceTimestamp(trace); const traceDuration = getTraceDuration(trace); @@ -184,7 +170,7 @@ export default class TracePageTimeline extends Component { return (
- +
} ({ + valueOffset: span.relativeStartTime, + valueWidth: span.duration, + serviceName: span.process.serviceName, + }))} /> {leftBound && should render a ', () => { - const wrapper = shallow(, defaultOptions); +it(' should render a ', () => { + const wrapper = shallow(, defaultOptions); expect(wrapper.find(SpanGraph).length).toBe(1); }); -it(' should render a ', () => { - const wrapper = shallow(, defaultOptions); +it(' should render a ', () => { + const wrapper = shallow(, defaultOptions); expect(wrapper.find(SpanGraphTickHeader).length).toBe(1); }); -it(' should just return a
if no trace is present', () => { - const wrapper = shallow(, defaultOptions); +it(' should just return a
if no trace is present', () => { + const wrapper = shallow(, defaultOptions); expect(wrapper.matchesElement(
)).toBeTruthy(); }); -it(' should render a filtering box if leftBound exists', () => { - const wrapper = shallow(, { +it(' should render a filtering box if leftBound exists', () => { + const wrapper = shallow(, { ...defaultOptions, context: { ...defaultOptions.context, @@ -116,8 +116,8 @@ it(' should render a filtering box if leftBound exists', () ).toBeTruthy(); }); -it(' should render a filtering box if rightBound exists', () => { - const wrapper = shallow(, { +it(' should render a filtering box if rightBound exists', () => { + const wrapper = shallow(, { ...defaultOptions, context: { ...defaultOptions.context, @@ -132,8 +132,8 @@ it(' should render a filtering box if rightBound exists', ( ).toBeTruthy(); }); -it(' should render handles for the timeRangeFilter', () => { - const wrapper = shallow(, defaultOptions); +it(' should render handles for the timeRangeFilter', () => { + const wrapper = shallow(, defaultOptions); expect( wrapper.containsMatchingElement( @@ -147,8 +147,8 @@ it(' should render handles for the timeRangeFilter', () => ).toBeTruthy(); }); -it(' should call startDragging for the leftBound handle', () => { - const wrapper = shallow(, defaultOptions); +it(' should call startDragging for the leftBound handle', () => { + const wrapper = shallow(, defaultOptions); const event = { clientX: 50 }; sinon.stub(wrapper.instance(), 'startDragging'); @@ -158,8 +158,8 @@ it(' should call startDragging for the leftBound handle', ( expect(wrapper.instance().startDragging.calledWith('leftBound', event)).toBeTruthy(); }); -it(' should call startDragging for the rightBound handle', () => { - const wrapper = shallow(, defaultOptions); +it(' should call startDragging for the rightBound handle', () => { + const wrapper = shallow(, defaultOptions); const event = { clientX: 50 }; sinon.stub(wrapper.instance(), 'startDragging'); @@ -169,8 +169,8 @@ it(' should call startDragging for the rightBound handle', expect(wrapper.instance().startDragging.calledWith('rightBound', event)).toBeTruthy(); }); -it(' should render without handles if no filtering', () => { - const wrapper = shallow(, { +it(' should render without handles if no filtering', () => { + const wrapper = shallow(, { ...defaultOptions, context: { ...defaultOptions.context, @@ -182,7 +182,7 @@ it(' should render without handles if no filtering', () => expect(wrapper.find(TimelineScrubber).length).toBe(0); }); -it(' should timeline-sort the trace before rendering', () => { +it(' should timeline-sort the trace before rendering', () => { // manually sort the spans in the defaultProps. const sortedTraceSpans = [ defaultProps.trace.spans[2], @@ -190,13 +190,13 @@ it(' should timeline-sort the trace before rendering', () = defaultProps.trace.spans[1], ]; - const wrapper = shallow(, defaultOptions); + const wrapper = shallow(, defaultOptions); const spanGraph = wrapper.find(SpanGraph).first(); expect(spanGraph.prop('spans')).toEqual(sortedTraceSpans); }); -it(' should create ticks and pass them to components', () => { +it(' should create ticks and pass them to components', () => { // manually build a ticks object for the trace const ticks = [ { timestamp, width: 2 }, @@ -207,7 +207,7 @@ it(' should create ticks and pass them to components', () = { timestamp: timestamp + 50000, width: 2 }, ]; - const wrapper = shallow(, defaultOptions); + const wrapper = shallow(, defaultOptions); const spanGraph = wrapper.find(SpanGraph).first(); const spanGraphTickHeader = wrapper.find(SpanGraphTickHeader).first(); @@ -215,29 +215,29 @@ it(' should create ticks and pass them to components', () = expect(spanGraphTickHeader.prop('ticks')).toEqual(ticks); }); -it(' should calculate the rowHeight', () => { - const wrapper = shallow(, defaultOptions); +it(' should calculate the rowHeight', () => { + const wrapper = shallow(, defaultOptions); const spanGraph = wrapper.find(SpanGraph).first(); expect(spanGraph.prop('rowHeight')).toBe(50 / 3); }); -it(' should pass the props through to SpanGraph', () => { - const wrapper = shallow(, defaultOptions); +it(' should pass the props through to SpanGraph', () => { + const wrapper = shallow(, defaultOptions); const spanGraph = wrapper.find(SpanGraph).first(); expect(spanGraph.prop('rowPadding')).toBe(0); }); -it(' should pass the props through to SpanGraphTickHeader', () => { - const wrapper = shallow(, defaultOptions); +it(' should pass the props through to SpanGraphTickHeader', () => { + const wrapper = shallow(, defaultOptions); const spanGraphTickHeader = wrapper.find(SpanGraphTickHeader).first(); expect(spanGraphTickHeader.prop('trace')).toEqual(defaultProps.trace); }); -it('TracePageTimeline.shouldComponentUpdate should return true for new timeRange', () => { - const wrapper = shallow(, defaultOptions); +it('TraceSpanGraph.shouldComponentUpdate should return true for new timeRange', () => { + const wrapper = shallow(, defaultOptions); expect( wrapper.instance().shouldComponentUpdate(defaultProps, wrapper.state(), { @@ -246,8 +246,8 @@ it('TracePageTimeline.shouldComponentUpdate should return true for new timeRange ).toBe(true); }); -it('TracePageTimeline.shouldComponentUpdate should return true for new traces', () => { - const wrapper = shallow(, defaultOptions); +it('TraceSpanGraph.shouldComponentUpdate should return true for new traces', () => { + const wrapper = shallow(, defaultOptions); expect( wrapper @@ -260,8 +260,8 @@ it('TracePageTimeline.shouldComponentUpdate should return true for new traces', ).toBe(true); }); -it('TracePageTimeline.shouldComponentUpdate should return true for currentlyDragging', () => { - const wrapper = shallow(, defaultOptions); +it('TraceSpanGraph.shouldComponentUpdate should return true for currentlyDragging', () => { + const wrapper = shallow(, defaultOptions); expect( wrapper.instance().shouldComponentUpdate( @@ -275,17 +275,17 @@ it('TracePageTimeline.shouldComponentUpdate should return true for currentlyDrag ).toBe(true); }); -it('TracePageTimeline.shouldComponentUpdate should return false otherwise', () => { - const wrapper = shallow(, defaultOptions); +it('TraceSpanGraph.shouldComponentUpdate should return false otherwise', () => { + const wrapper = shallow(, defaultOptions); expect( wrapper.instance().shouldComponentUpdate(defaultProps, wrapper.state(), defaultOptions.context) ).toBe(false); }); -it('TracePageTimeline.onMouseMove should do nothing if currentlyDragging is false', () => { +it('TraceSpanGraph.onMouseMove should do nothing if currentlyDragging is false', () => { const updateTimeRangeFilter = sinon.spy(); - const wrapper = shallow(, { + const wrapper = shallow(, { ...defaultOptions, context: { ...defaultOptions.context, @@ -302,8 +302,8 @@ it('TracePageTimeline.onMouseMove should do nothing if currentlyDragging is fals expect(updateTimeRangeFilter.called).toBeFalsy(); }); -it('TracePageTimeline.onMouseMove should store the clientX on the state', () => { - const wrapper = shallow(, defaultOptions); +it('TraceSpanGraph.onMouseMove should store the clientX on the state', () => { + const wrapper = shallow(, defaultOptions); wrapper.instance().svg = { clientWidth: 100 }; wrapper.setState({ currentlyDragging: 'leftBound' }); @@ -313,9 +313,9 @@ it('TracePageTimeline.onMouseMove should store the clientX on the state', () => expect(wrapper.state('prevX')).toBe(45); }); -it('TracePageTimeline.onMouseMove should update the timeRangeFilter for the left handle', () => { +it('TraceSpanGraph.onMouseMove should update the timeRangeFilter for the left handle', () => { const updateTimeRangeFilter = sinon.spy(); - const wrapper = shallow(, { + const wrapper = shallow(, { ...defaultOptions, context: { ...defaultOptions.context, @@ -331,9 +331,9 @@ it('TracePageTimeline.onMouseMove should update the timeRangeFilter for the left expect(updateTimeRangeFilter.calledWith(timestamp + 22500, timestamp + 50000)).toBeTruthy(); }); -it('TracePageTimeline.onMouseMove should update the timeRangeFilter for the right handle', () => { +it('TraceSpanGraph.onMouseMove should update the timeRangeFilter for the right handle', () => { const updateTimeRangeFilter = sinon.spy(); - const wrapper = shallow(, { + const wrapper = shallow(, { ...defaultOptions, context: { ...defaultOptions.context, @@ -349,8 +349,8 @@ it('TracePageTimeline.onMouseMove should update the timeRangeFilter for the righ expect(updateTimeRangeFilter.calledWith(timestamp, timestamp + 22500)).toBeTruthy(); }); -it('TracePageTimeline.startDragging should store the boundName and the prevX in state', () => { - const wrapper = shallow(, defaultOptions); +it('TraceSpanGraph.startDragging should store the boundName and the prevX in state', () => { + const wrapper = shallow(, defaultOptions); wrapper.instance().startDragging('leftBound', { clientX: 100 }); @@ -359,8 +359,8 @@ it('TracePageTimeline.startDragging should store the boundName and the prevX in }); // TODO: Need to figure out how to mock to window events. -it.skip('TracePageTimeline.startDragging should bind event listeners to the window', () => { - const wrapper = shallow(, defaultOptions); +it.skip('TraceSpanGraph.startDragging should bind event listeners to the window', () => { + const wrapper = shallow(, defaultOptions); clearListeners(); @@ -370,8 +370,8 @@ it.skip('TracePageTimeline.startDragging should bind event listeners to the wind expect(addEventListener.calledWith('mouseup', sinon.match.func)).toBeTruthy(); }); -it.skip('TracePageTimeline.startDragging should call onMouseMove on the window', () => { - const wrapper = shallow(, defaultOptions); +it.skip('TraceSpanGraph.startDragging should call onMouseMove on the window', () => { + const wrapper = shallow(, defaultOptions); clearListeners(); @@ -384,8 +384,8 @@ it.skip('TracePageTimeline.startDragging should call onMouseMove on the window', expect(wrapper.instance().onMouseMove.calledWith(event)).toBeTruthy(); }); -it.skip('TracePageTimeline.startDragging mouseup should call stopDragging', () => { - const wrapper = shallow(, defaultOptions); +it.skip('TraceSpanGraph.startDragging mouseup should call stopDragging', () => { + const wrapper = shallow(, defaultOptions); clearListeners(); @@ -398,8 +398,8 @@ it.skip('TracePageTimeline.startDragging mouseup should call stopDragging', () = expect(wrapper.instance().stopDragging.called).toBeTruthy(); }); -it.skip('TracePageTimeline.startDragging mouseup should stop listening to the events', () => { - const wrapper = shallow(, defaultOptions); +it.skip('TraceSpanGraph.startDragging mouseup should stop listening to the events', () => { + const wrapper = shallow(, defaultOptions); clearListeners(); @@ -412,8 +412,8 @@ it.skip('TracePageTimeline.startDragging mouseup should stop listening to the ev expect(removeEventListener.calledWith('mouseup', sinon.match.func)).toBeTruthy(); }); -it('TracePageTimeline.stopDragging should clear currentlyDragging and prevX', () => { - const wrapper = shallow(, defaultOptions); +it('TraceSpanGraph.stopDragging should clear currentlyDragging and prevX', () => { + const wrapper = shallow(, defaultOptions); wrapper.instance().stopDragging(); diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBar.js b/src/components/TracePage/TraceTimelineViewer/SpanBar.js new file mode 100644 index 0000000000..44b158a422 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/SpanBar.js @@ -0,0 +1,118 @@ +// 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 PropTypes from 'prop-types'; +import React from 'react'; +import { onlyUpdateForKeys, compose, withState, withProps } from 'recompose'; + +import './grid.css'; +import './index.css'; +import { clampValue } from './utils'; + +const clampPercent = clampValue.bind(null, 0, 100); + +function SpanBarInternal(props) { + const { + startPercent, + endPercent, + color, + label, + enableTransition, + onClick, + onMouseOver, + onMouseOut, + childInterval, + } = props; + const barWidth = endPercent - startPercent; + const barHeightPercent = 50; + return ( +
+
+ {childInterval && +
} +
+
+ ); +} + +SpanBarInternal.defaultProps = { + enableTransition: true, +}; + +SpanBarInternal.propTypes = { + childInterval: PropTypes.shape({ + startPercent: PropTypes.number, + endPercent: PropTypes.number, + color: PropTypes.string, + }), + enableTransition: PropTypes.bool, + startPercent: PropTypes.number.isRequired, + endPercent: PropTypes.number.isRequired, + color: PropTypes.string, + label: PropTypes.string, + onClick: PropTypes.func, + onMouseOver: PropTypes.func, + onMouseOut: PropTypes.func, +}; + +const SpanBar = compose( + withState('label', 'setLabel', props => props.shortLabel), + withProps(({ setLabel, shortLabel, longLabel }) => ({ + onMouseOver: () => setLabel(longLabel), + onMouseOut: () => setLabel(shortLabel), + })), + onlyUpdateForKeys(['startPercent', 'endPercent', 'label', 'childInterval']) +)(SpanBarInternal); + +export default SpanBar; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBreakdownGraph.js b/src/components/TracePage/TraceTimelineViewer/SpanBreakdownGraph.js deleted file mode 100644 index 4f21738869..0000000000 --- a/src/components/TracePage/TraceTimelineViewer/SpanBreakdownGraph.js +++ /dev/null @@ -1,87 +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 PropTypes from 'prop-types'; -import React from 'react'; -import SpanGraphTick from '../../SpanGraph/SpanGraphTick'; -import logPropTypes from '../../../propTypes/log'; - -export default function SpanBreakdownBar({ span }) { - const padding = 3; - const fontSize = 11; - return ( -
-
- - 0 ms - - - {span.duration} ms - -
- - {span.logs.map((log, i) => - - )} - -
- ); -} - -SpanBreakdownBar.propTypes = { - span: PropTypes.shape({ - duration: PropTypes.number, - timestamp: PropTypes.number, - logs: PropTypes.arrayOf(logPropTypes), - }), -}; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail.js b/src/components/TracePage/TraceTimelineViewer/SpanDetail.js index d34d383d45..ecf6303256 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetail.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetail.js @@ -54,10 +54,10 @@ const CollapsePanelStatefull = collapseEnhancer(CollapsePanel); function ExpandableDataTable(props) { const { data, label, open, onToggleOpen } = props; return ( -
+
onToggleOpen(!open)} - className="overflow-hidden nowrap inline" + className="overflow-hidden nowrap" style={{ cursor: 'pointer', textOverflow: 'ellipsis', @@ -70,8 +70,8 @@ function ExpandableDataTable(props) { {!open && - {data.map(row => - + {data.map((row, i) => + {row.key}= @@ -81,32 +81,31 @@ function ExpandableDataTable(props) { }
{open && - - - {data.map(row => { - let json; - try { - json = JSON.parse(row.value); - } catch (e) { - json = row.value; - } - return ( - - - - - ); - })} - -
- {row.key} - -
-
} +
+ + + {data.map((row, i) => { + let json; + try { + json = JSON.parse(row.value); + } catch (e) { + json = row.value; + } + return ( + // `i` is necessary in the key because row.key can repeat + + + + + ); + })} + +
+ {row.key} + +
+
+
}
); } @@ -116,10 +115,13 @@ ExpandableDataTable.defaultProps = { }; ExpandableDataTable.propTypes = { open: PropTypes.bool, - data: PropTypes.shape({ - key: PropTypes.string, - value: PropTypes.value, - }), + data: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string, + type: PropTypes.string, + value: PropTypes.value, + }) + ), label: PropTypes.string, onToggleOpen: PropTypes.func, }; @@ -128,15 +130,13 @@ const ExpandableDataTableStatefull = collapseEnhancer(ExpandableDataTable); function Logs({ logs, traceStartTime, open, onToggleOpen }) { return (
- + onToggleOpen(!open)} style={{ opacity: 1 }}> + + Logs ({logs.length}) + {open &&
{_.sortBy(logs, 'timestamp').map(log => @@ -224,6 +224,6 @@ export default function SpanDetail(props) { ); } SpanDetail.propTypes = { - span: PropTypes.obj, - trace: PropTypes.obj, + span: PropTypes.object, + trace: PropTypes.object, }; diff --git a/src/components/TracePage/TraceTimelineViewer/Ticks.js b/src/components/TracePage/TraceTimelineViewer/Ticks.js new file mode 100644 index 0000000000..aa20076e07 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/Ticks.js @@ -0,0 +1,61 @@ +// 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 PropTypes from 'prop-types'; +import React from 'react'; + +export default function Ticks(props) { + const { ticks } = props; + const margin = 5; + return ( +
+ {ticks.map(tick => +
+ + {tick.label} + +
+ )} +
+ ); +} +Ticks.propTypes = { + ticks: PropTypes.arrayOf( + PropTypes.shape({ + percent: PropTypes.number, + label: PropTypes.string, + }) + ).isRequired, +}; diff --git a/src/components/SpanGraph/SpanGraphTickHeaderLabel.test.js b/src/components/TracePage/TraceTimelineViewer/TimelineRow.js similarity index 50% rename from src/components/SpanGraph/SpanGraphTickHeaderLabel.test.js rename to src/components/TracePage/TraceTimelineViewer/TimelineRow.js index fff37d23ec..57308a2861 100644 --- a/src/components/SpanGraph/SpanGraphTickHeaderLabel.test.js +++ b/src/components/TracePage/TraceTimelineViewer/TimelineRow.js @@ -18,45 +18,49 @@ // 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 { shallow } from 'enzyme'; - -import SpanGraphTickHeaderLabel from './SpanGraphTickHeaderLabel'; -import { formatDurationForTrace } from '../../selectors/trace'; - -const timestamp = new Date().getTime() * 1000; -const duration = 1000000; -const trace = { - spans: [ - { - duration: 4000000, - timestamp, - }, - ], -}; -const defaultProps = { - style: { left: 100 }, - duration, - trace, + +export default function TimelineRow(props) { + const { children, className, ...rest } = props; + return ( +
+ {children} +
+ ); +} + +TimelineRow.propTypes = { + children: PropTypes.node, + className: PropTypes.string, }; -it(' should render a
', () => { - const wrapper = shallow(); - const divs = wrapper.find('div'); +function TimelineRowLeft(props) { + const { children, ...rest } = props; + return ( +
+ {children} +
+ ); +} - expect(divs.length).toBe(1); -}); +TimelineRowLeft.propTypes = { + children: PropTypes.node, +}; -it(' should render the duration as text', () => { - const wrapper = shallow(); - const div = wrapper.find('div').first(); +TimelineRow.Left = TimelineRowLeft; - expect(div.prop('children')).toBe(formatDurationForTrace({ trace, duration })); -}); +function TimelineRowRight(props) { + const { children, ...rest } = props; + return ( +
+ {children} +
+ ); +} -it(' should pass the style through', () => { - const wrapper = shallow(); - const firstTick = wrapper.find('div').first(); +TimelineRowRight.propTypes = { + children: PropTypes.node, +}; - expect(firstTick.prop('style')).toEqual(defaultProps.style); -}); +TimelineRow.Right = TimelineRowRight; diff --git a/src/components/TracePage/TraceTimelineViewer/index.js b/src/components/TracePage/TraceTimelineViewer/index.js index ecec067886..9e472d033f 100644 --- a/src/components/TracePage/TraceTimelineViewer/index.js +++ b/src/components/TracePage/TraceTimelineViewer/index.js @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; -import { onlyUpdateForKeys, compose, withState, withProps, pure } from 'recompose'; +import { pure } from 'recompose'; import * as d3 from 'd3-scale'; import { filterSpansForText } from '../../../selectors/span'; import './grid.css'; @@ -29,181 +29,29 @@ import './index.css'; import { calculateSpanPosition, convertTimeRangeToPercent, - ensureWithinRange, + // ensureWithinRange, formatDuration, findServerChildSpan, isErrorSpan, spanContainsErredSpan, } from './utils'; -import { transformTrace } from './transforms'; +// import { transformTrace } from './transforms'; import colorGenerator from '../../../utils/color-generator'; import SpanDetail from './SpanDetail'; +import Ticks from './Ticks'; +import SpanBar from './SpanBar'; +import TimelineRow from './TimelineRow'; // TODO: Move some styles to css // TODO: Clean up component names and move to seperate files. // TODO: Add unit tests // TODO: unify transforms and utils -const ensurePercentIsBetween0And100 = percent => ensureWithinRange([0, 100], percent); - -/** - * Components! - */ function Rail() { return ; } -function SpanBar(props) { - const { - startPercent, - endPercent, - color, - label, - enableTransition, - onClick = noop => noop, - onMouseOver = noop => noop, - onMouseOut = noop => noop, - childInterval, - } = props; - const barWidth = endPercent - startPercent; - const barHeightPercent = 50; - return ( -
onMouseOver()} - onMouseOut={() => onMouseOut()} - className="span-row__bar hint--right hint--always" - onClick={onClick} - aria-label={label} - style={{ - transition: enableTransition ? 'width 500ms' : undefined, - borderRadius: 3, - position: 'absolute', - display: 'inline-block', - backgroundColor: color, - top: `${(100 - barHeightPercent) / 2}%`, - height: `${barHeightPercent}%`, - width: `${ensurePercentIsBetween0And100(barWidth)}%`, - left: `${ensurePercentIsBetween0And100(startPercent)}%`, - }} - > - {childInterval && -
} -
- ); -} -SpanBar.defaultProps = { - enableTransition: true, -}; -SpanBar.propTypes = { - childInterval: PropTypes.shape({ - startPercent: PropTypes.number, - endPercent: PropTypes.number, - color: PropTypes.string, - }), - enableTransition: PropTypes.bool, - startPercent: PropTypes.number.isRequired, - endPercent: PropTypes.number.isRequired, - color: PropTypes.string, - label: PropTypes.string, - - onClick: PropTypes.func, - onMouseOver: PropTypes.func, - onMouseOut: PropTypes.func, -}; -const SpanBarEnhanced = compose( - withState('label', 'setLabel', props => props.shortLabel), - withProps(({ setLabel, shortLabel, longLabel }) => ({ - onMouseOver: () => setLabel(longLabel), - onMouseOut: () => setLabel(shortLabel), - })), - onlyUpdateForKeys(['startPercent', 'endPercent', 'label', 'childInterval']) -)(SpanBar); - -function Ticks(props) { - const { ticks } = props; - const margin = 5; - return ( -
- {ticks.map(tick => -
- - {tick.label} - -
- )} -
- ); -} -Ticks.propTypes = { - ticks: PropTypes.arrayOf( - PropTypes.shape({ - percent: PropTypes.number, - label: PropTypes.string, - }) - ), -}; - -const TimelineRow = props => { - const { children, className, ...rest } = props; - return ( -
- {children} -
- ); -}; -TimelineRow.propTypes = { - children: PropTypes.node, - className: PropTypes.string, -}; -TimelineRow.Left = props => { - const { children, ...rest } = props; - return ( -
- {children} -
- ); -}; -TimelineRow.Left.propTypes = { - children: PropTypes.node, -}; -TimelineRow.Right = props => { - const { children, ...rest } = props; - return ( -
- {children} -
- ); -}; -TimelineRow.Right.propTypes = { - children: PropTypes.node, -}; - -const Timeline = {}; -Timeline.SpanDetails = pure(props => { +const TimelineSpanDetailRow = pure(props => { const { span, color, trace } = props; const { spanID } = span; return ( @@ -226,7 +74,8 @@ Timeline.SpanDetails = pure(props => { ); }); -Timeline.SpanDetails.propTypes = { + +TimelineSpanDetailRow.propTypes = { span: PropTypes.object, color: PropTypes.string, }; @@ -380,7 +229,7 @@ function TraceView(props) { onSpanClick(spanID)}> ({ percent }))} /> - ); if (showSpanDetails) { - arr.push(); + arr.push( + // + + ); } return arr; }, []); @@ -421,7 +273,6 @@ function TraceView(props) { } TraceView.propTypes = { trace: PropTypes.object, - collapsedSpanIDs: PropTypes.object, selectedSpanIDs: PropTypes.object, filteredSpansIDs: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), @@ -436,16 +287,19 @@ TraceView.propTypes = { export default class TraceTimelineViewer extends Component { constructor(props) { super(props); - const initialDepthCollapse = false; // Change this to = 1 to first children spans only. + // const initialDepthCollapse = false; // Change this to = 1 to first children spans only. const collapsedSpans = new Map(); - const transformedTrace = transformTrace(props.trace); - if (_.isNumber(initialDepthCollapse)) { - transformedTrace.spans.forEach(span => { - if (span.depth >= initialDepthCollapse && span.hasChildren) { - collapsedSpans.set(span.spanID, true); - } - }); - } + // const transformedTrace = transformTrace(props.trace); + const transformedTrace = props.xformedTrace; + // if (_.isNumber(initialDepthCollapse)) { + // transformedTrace.spans.forEach(span => { + // if (span.depth >= initialDepthCollapse && span.hasChildren) { + // collapsedSpans.set(span.spanID, true); + // } + // }); + // } + this.toggleSpanCollapse = this.toggleSpanCollapse.bind(this); + this.toggleSpanSelect = this.toggleSpanSelect.bind(this); this.state = { selectedSpans: new Map(), @@ -493,15 +347,16 @@ export default class TraceTimelineViewer extends Component { filteredSpansIDs={filteredSpansIDs} zoomStart={zoom[0]} zoomEnd={zoom[1]} - onSpanClick={spanID => this.toggleSpanSelect(spanID)} - onSpanCollapseClick={spanID => this.toggleSpanCollapse(spanID)} + onSpanClick={this.toggleSpanSelect} + onSpanCollapseClick={this.toggleSpanCollapse} />
); } } TraceTimelineViewer.propTypes = { - trace: PropTypes.object, + // trace: PropTypes.object, + xformedTrace: PropTypes.object, timeRangeFilter: PropTypes.array, textFilter: PropTypes.string, }; diff --git a/src/components/TracePage/TraceTimelineViewer/transforms.js b/src/components/TracePage/TraceTimelineViewer/transforms.js index 54dd3d0e01..a77413e029 100644 --- a/src/components/TracePage/TraceTimelineViewer/transforms.js +++ b/src/components/TracePage/TraceTimelineViewer/transforms.js @@ -18,20 +18,28 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import * as traceSelectors from '../../../selectors/trace'; +import { + getTraceDuration, + getTraceEndTimestamp, + getTraceSpanIdsAsTree, + getTraceSpansAsMap, + getTraceTimestamp, + hydrateSpansWithProcesses, +} from '../../../selectors/trace'; const cache = new Map(); + export function transformTrace(trace) { if (cache.has(trace.traceID)) { return cache.get(trace.traceID); } - traceSelectors.hydrateSpansWithProcesses(trace); - const Tree = traceSelectors.getTraceSpanIdsAsTree(trace); - const spanMap = traceSelectors.getTraceSpansAsMap(trace); + hydrateSpansWithProcesses(trace); + const traceStartTime = getTraceTimestamp(trace); + const traceEndTime = getTraceEndTimestamp(trace); + const tree = getTraceSpanIdsAsTree(trace); + const spanMap = getTraceSpansAsMap(trace); const spans = []; - const traceStartTime = traceSelectors.getTraceTimestamp(trace); - const traceEndTime = traceSelectors.getTraceEndTimestamp(trace); - Tree.walk((spanID, value) => { + tree.walk((spanID, node, depth) => { if (spanID === '__root__') { return; } @@ -39,16 +47,16 @@ export function transformTrace(trace) { spans.push({ ...span, relativeStartTime: span.startTime - traceStartTime, - depth: traceSelectors.getSpanDepthForTrace({ trace, span }) - 1, - hasChildren: value.children.length > 0, + depth: depth - 1, + hasChildren: node.children.length > 0, }); }); const transform = { ...trace, - duration: traceSelectors.getTraceDuration(trace), + spans, + duration: getTraceDuration(trace), startTime: traceStartTime, endTime: traceEndTime, - spans, }; cache.set(trace.traceID, transform); return transform; diff --git a/src/components/TracePage/TraceTimelineViewer/utils.js b/src/components/TracePage/TraceTimelineViewer/utils.js index 43903aba00..6818ffff6a 100644 --- a/src/components/TracePage/TraceTimelineViewer/utils.js +++ b/src/components/TracePage/TraceTimelineViewer/utils.js @@ -42,16 +42,16 @@ export function calculateSpanPosition({ }; } -/** - * Given a percent and traceDuration, will give back - * a relative time from 0. - * - * eg: 50% at 100ms = 50ms - */ -export function calculateTimeAtPositon({ position, traceDuration }) { - const xValue = d3.scaleLinear().domain([0, 100]).range([0, traceDuration]); - return xValue(position); -} +// /** +// * Given a percent and traceDuration, will give back +// * a relative time from 0. +// * +// * eg: 50% at 100ms = 50ms +// */ +// export function calculateTimeAtPositon({ position, traceDuration }) { +// const xValue = d3.scaleLinear().domain([0, 100]).range([0, traceDuration]); +// return xValue(position); +// } /** * Given a subset of the duration of two timestamps, @@ -69,16 +69,26 @@ export function convertTimeRangeToPercent([startTime, endTime], [traceStartTime, return [getPercent(startTime), getPercent(endTime)]; } -export function ensureWithinRange([floor = 0, ceiling = 100], num) { - if (num < floor) { - return floor; +export function clampValue(min, max, value) { + if (value <= min) { + return min; } - if (num > ceiling) { - return ceiling; + if (value >= max) { + return max; } - return num; + return value; } +// export function ensureWithinRange([floor = 0, ceiling = 100], num) { +// if (num < floor) { +// return floor; +// } +// if (num > ceiling) { +// return ceiling; +// } +// return num; +// } + export function hasTagKey(tags, key, value) { if (!tags || !tags.length) { return false; @@ -122,15 +132,14 @@ export function findServerChildSpan(spans) { } const span = spans[0]; const spanChildDepth = span.depth + 1; - let serverSpan; let i = 1; - while (i < spans.length && spans[i].depth === spanChildDepth && !serverSpan) { + while (i < spans.length && spans[i].depth === spanChildDepth) { if (isServerSpan(spans[i])) { - serverSpan = spans[i]; + return spans[i]; } i++; } - return serverSpan; + return null; } export { formatDuration } from '../../../utils/date'; diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index 891f792b0d..f946531913 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -23,10 +23,15 @@ import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { Sticky } from 'react-sticky'; +import _maxBy from 'lodash/maxBy'; +import _values from 'lodash/values'; import './TracePage.css'; +import { transformTrace } from './TraceTimelineViewer/transforms'; import * as jaegerApiActions from '../../actions/jaeger-api'; import colorGenerator from '../../utils/color-generator'; +// import { getTraceSummary } from '../../model/search'; +import { getTraceName } from '../../model/trace-viewer'; import { dropEmptyStartTimeSpans, @@ -38,7 +43,7 @@ import { import NotFound from '../App/NotFound'; import TracePageHeader from './TracePageHeader'; import TraceTimelineViewer from './TraceTimelineViewer'; -import TracePageTimeline from './TracePageTimeline'; +import TraceSpanGraph from './TraceSpanGraph'; import tracePropTypes from '../../propTypes/trace'; @@ -47,6 +52,7 @@ export default class TracePage extends Component { return { fetchTrace: PropTypes.func.isRequired, trace: tracePropTypes, + xformedTrace: PropTypes.object, loading: PropTypes.bool, id: PropTypes.string.isRequired, }; @@ -65,6 +71,7 @@ export default class TracePage extends Component { constructor(props) { super(props); this.state = { textFilter: '', timeRangeFilter: [], slimView: false }; + this.toggleSlimView = this.toggleSlimView.bind(this); } getChildContext() { @@ -113,8 +120,8 @@ export default class TracePage extends Component { this.setState({ timeRangeFilter }); } - toggleSlimView(slimView) { - this.setState({ slimView }); + toggleSlimView() { + this.setState({ slimView: !this.state.slimView }); // fix issue #12 - TraceView header expander not working correctly // TODO: evaluate alternatives to react-sticky setTimeout(() => this.forceUpdate(), 0); @@ -129,7 +136,7 @@ export default class TracePage extends Component { } render() { - const { id, trace } = this.props; + const { id, trace, xformedTrace } = this.props; const { slimView } = this.state; if (!trace) { @@ -140,6 +147,10 @@ export default class TracePage extends Component { return ; } + const { duration, processes, spans, startTime, traceID } = xformedTrace; + const maxSpanDepth = _maxBy(spans, 'depth').depth + 1; + const numberOfServices = new Set(_values(processes).map(p => p.serviceName)).size; + return (
this.toggleSlimView(!slimView)} + timestampMs={startTime / 1000} + traceID={traceID} + onSlimViewClicked={this.toggleSlimView} /> - {!slimView && } + {!slimView && }
@@ -170,11 +188,13 @@ export default class TracePage extends Component { function mapStateToProps(state, ownProps) { const { id } = ownProps.params; let trace = state.trace.traces[id]; + let xformedTrace; if (trace && !(trace instanceof Error)) { trace = dropEmptyStartTimeSpans(trace); trace = hydrateSpansWithProcesses(trace); + xformedTrace = transformTrace(trace); } - return { id, trace, loading: state.trace.loading }; + return { id, trace, xformedTrace, loading: state.trace.loading }; } function mapDispatchToProps(dispatch) { diff --git a/src/components/TracePage/index.test.js b/src/components/TracePage/index.test.js index 8806da109b..30f51011be 100644 --- a/src/components/TracePage/index.test.js +++ b/src/components/TracePage/index.test.js @@ -23,7 +23,7 @@ import sinon from 'sinon'; import { shallow } from 'enzyme'; import TracePage from '../../../src/components/TracePage'; import TracePageHeader from '../../../src/components/TracePage/TracePageHeader'; -import TracePageTimeline from '../../../src/components/TracePage/TracePageTimeline'; +import TraceSpanGraph from '../../../src/components/TracePage/TraceSpanGraph'; const traceID = 'trace-id'; const timestamp = new Date().getTime() * 1000; @@ -72,10 +72,10 @@ it(' should render a with the trace', () => { expect(wrapper.find(TracePageHeader).get(0)).toBeTruthy(); }); -it(' should render a with the trace', () => { +it(' should render a with the trace', () => { const wrapper = shallow(); - expect(wrapper.contains()).toBeTruthy(); + expect(wrapper.contains()).toBeTruthy(); }); it(' should render an empty page if no trace', () => { diff --git a/src/index.js b/src/index.js index 984724f223..71ce057ddc 100644 --- a/src/index.js +++ b/src/index.js @@ -26,7 +26,7 @@ import 'basscss/css/basscss.css'; import JaegerUIApp from './components/App'; -export { default as SpanGraph } from './components/SpanGraph'; +export { default as SpanGraph } from './components/TracePage/SpanGraph'; export { default as TracePage } from './components/TracePage'; export { SearchTracePage } from './components/SearchTracePage'; export default JaegerUIApp; diff --git a/src/components/SpanGraph/SpanGraphTickHeaderLabel.js b/src/model/trace-viewer.js similarity index 67% rename from src/components/SpanGraph/SpanGraphTickHeaderLabel.js rename to src/model/trace-viewer.js index 177e5e0809..19c9089dba 100644 --- a/src/components/SpanGraph/SpanGraphTickHeaderLabel.js +++ b/src/model/trace-viewer.js @@ -18,22 +18,7 @@ // 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 tracePropTypes from '../../propTypes/trace'; -import { formatDurationForTrace } from '../../selectors/trace'; - -export default function SpanGraphTickHeaderLabel({ duration, style, trace }) { - return ( -
- {formatDurationForTrace({ trace, duration })} -
- ); +export function getTraceName(spans, processes) { + const span = spans.find(sp => sp.spanID === sp.traceID) || spans[0]; + return span ? `${processes[span.processID].serviceName}: ${span.operationName}` : ''; } - -SpanGraphTickHeaderLabel.propTypes = { - style: PropTypes.object, - trace: tracePropTypes.isRequired, - duration: PropTypes.number.isRequired, -}; diff --git a/src/utils/TreeNode.js b/src/utils/TreeNode.js index e889c59292..d0ecf8d6cc 100644 --- a/src/utils/TreeNode.js +++ b/src/utils/TreeNode.js @@ -19,8 +19,8 @@ // THE SOFTWARE. export default class TreeNode { - static iterFunction(fn) { - return node => fn(node.value, node); + static iterFunction(fn, depth = 0) { + return node => fn(node.value, node, depth); } static searchFunction(search) { @@ -100,8 +100,8 @@ export default class TreeNode { return findPath(this, []); } - walk(fn) { - TreeNode.iterFunction(fn)(this); - this.children.forEach(child => child.walk(fn)); + walk(fn, depth = 0) { + TreeNode.iterFunction(fn, depth)(this); + this.children.forEach(child => child.walk(fn, depth + 1)); } } From 4fa5c8a95050db1fdf48f32cef0c9ac57b0c6192 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Mon, 14 Aug 2017 02:45:43 -0400 Subject: [PATCH 03/20] WIP commit for refactoring trace detail view TODO - Test are currently in a bad state - Finish shifting to transformed trace, exclusively, instead of raw trace - Misc cleanup (unused / ordering of vars, imports, etc) is outstanding while changes are WIP - Continued refinement of trace-detail components, selectors and utils - Many styles will be moved to CSS - Will likely remove `model/trace-viewer.js` Span Bar / Detail - Label on span bars no longer off-screen - Clip or hide span bars when zoomed in (insted of flush left) - Add shadow to left / right boundary when span bar view is clipped - Darkened span name column to differentiate from span bar section - Span detail left column color coded by service - Clicking span detail left column collapses detail - Clicking anywhere left of parent span name toggles children visibility --- package.json | 1 + src/components/TracePage/TimelineScrubber.js | 2 - src/components/TracePage/TracePage.css | 21 +- src/components/TracePage/TraceSpanGraph.js | 4 +- .../TracePage/TraceTimelineViewer/SpanBar.css | 29 ++ .../TracePage/TraceTimelineViewer/SpanBar.js | 108 +++----- .../TraceTimelineViewer/SpanBarRow.js | 164 +++++++++++ .../TraceTimelineViewer/SpanDetail.js | 3 + .../TraceTimelineViewer/SpanDetailRow.js | 68 +++++ .../TraceTimelineViewer/SpanTreeOffset.css | 22 ++ .../TraceTimelineViewer/SpanTreeOffset.js | 37 +++ .../TracePage/TraceTimelineViewer/Ticks.css | 17 ++ .../TracePage/TraceTimelineViewer/Ticks.js | 43 ++- .../TraceTimelineViewer/TimelineRow.js | 37 +-- .../TracePage/TraceTimelineViewer/index.css | 109 +++++++- .../TracePage/TraceTimelineViewer/index.js | 255 ++++++------------ .../TracePage/TraceTimelineViewer/utils.js | 36 ++- yarn.lock | 2 +- 18 files changed, 651 insertions(+), 307 deletions(-) create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanBar.css create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanBarRow.js create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.css create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.js create mode 100644 src/components/TracePage/TraceTimelineViewer/Ticks.css diff --git a/package.json b/package.json index a8e6e344b1..7c480e1002 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "basscss": "^8.0.3", "chance": "^1.0.4", + "classnames": "^2.2.5", "cytoscape": "^2.7.13", "cytoscape-dagre": "^1.3.0", "d3-scale": "^1.0.4", diff --git a/src/components/TracePage/TimelineScrubber.js b/src/components/TracePage/TimelineScrubber.js index 86d9a2f42f..e9fa69917c 100644 --- a/src/components/TracePage/TimelineScrubber.js +++ b/src/components/TracePage/TimelineScrubber.js @@ -28,7 +28,6 @@ import { getPercentageOfInterval } from '../../utils/date'; const HANDLE_WIDTH = 6; const HANDLE_HEIGHT = 20; const HANDLE_TOP_OFFSET = 0; -const LINE_WIDTH = 2; export default function TimelineScrubber({ trace, @@ -50,7 +49,6 @@ export default function TimelineScrubber({ y2="100%" x1={`${xPercentage}%`} x2={`${xPercentage}%`} - strokeWidth={LINE_WIDTH} /> this.startDragging('leftBound', ...args)} />} {rightBound && @@ -227,7 +227,7 @@ export default class TraceSpanGraph extends Component { timestamp={rightBound} handleWidth={8} handleHeight={30} - handleTopOffset={10} + handleTopOffset={15} onMouseDown={(...args) => this.startDragging('rightBound', ...args)} />} diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBar.css b/src/components/TracePage/TraceTimelineViewer/SpanBar.css new file mode 100644 index 0000000000..750ed4c976 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/SpanBar.css @@ -0,0 +1,29 @@ + +.span-bar-wrapper { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + overflow: hidden; + opacity: 0.5; +} + +.span-row.is-expanded .span-bar-wrapper, +.span-row:hover .span-bar-wrapper { + opacity: 1; +} + +.span-bar { + border-radius: 3px; + position: absolute; + height: 50%; + top: 25%; +} + +.span-bar-rpc { + position: absolute; + top: 35%; + bottom: 35%; + z-index: 1; +} diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBar.js b/src/components/TracePage/TraceTimelineViewer/SpanBar.js index 44b158a422..93795e5348 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBar.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanBar.js @@ -22,97 +22,69 @@ import PropTypes from 'prop-types'; import React from 'react'; import { onlyUpdateForKeys, compose, withState, withProps } from 'recompose'; -import './grid.css'; -import './index.css'; -import { clampValue } from './utils'; +import './SpanBar.css'; -const clampPercent = clampValue.bind(null, 0, 100); +function toPercent(value) { + return `${value * 100}%`; +} + +function SpanBarImpl(props) { + const { viewEnd, viewStart, color, label, hintSide, onClick, onMouseOver, onMouseOut, rpc } = props; -function SpanBarInternal(props) { - const { - startPercent, - endPercent, - color, - label, - enableTransition, - onClick, - onMouseOver, - onMouseOut, - childInterval, - } = props; - const barWidth = endPercent - startPercent; - const barHeightPercent = 50; return ( -
+
- {childInterval && -
} -
+ /> + {rpc && +
}
); } -SpanBarInternal.defaultProps = { - enableTransition: true, -}; - -SpanBarInternal.propTypes = { - childInterval: PropTypes.shape({ - startPercent: PropTypes.number, - endPercent: PropTypes.number, +SpanBarImpl.propTypes = { + rpc: PropTypes.shape({ + viewStart: PropTypes.number, + viewEnd: PropTypes.number, color: PropTypes.string, }), - enableTransition: PropTypes.bool, - startPercent: PropTypes.number.isRequired, - endPercent: PropTypes.number.isRequired, - color: PropTypes.string, - label: PropTypes.string, + viewStart: PropTypes.number.isRequired, + viewEnd: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + hintSide: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, onClick: PropTypes.func, onMouseOver: PropTypes.func, onMouseOut: PropTypes.func, }; +SpanBarImpl.defaultProps = { + rpc: null, + onClick: null, + onMouseOver: null, + onMouseOut: null, +}; + const SpanBar = compose( withState('label', 'setLabel', props => props.shortLabel), withProps(({ setLabel, shortLabel, longLabel }) => ({ onMouseOver: () => setLabel(longLabel), onMouseOut: () => setLabel(shortLabel), })), - onlyUpdateForKeys(['startPercent', 'endPercent', 'label', 'childInterval']) -)(SpanBarInternal); + onlyUpdateForKeys(['viewStart', 'viewEnd', 'label', 'rpc']) +)(SpanBarImpl); export default SpanBar; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js new file mode 100644 index 0000000000..545100a467 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js @@ -0,0 +1,164 @@ +// 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 PropTypes from 'prop-types'; +import React from 'react'; + +import TimelineRow from './TimelineRow'; +import SpanTreeOffset from './SpanTreeOffset'; +import SpanBar from './SpanBar'; +import Ticks from './Ticks'; + +export default function SpanBarRow(props) { + const { + className, + color, + depth, + isChildrenExpanded, + isDetailExapnded, + isFilteredOut, + isParent, + label, + onDetailToggled, + onChildrenToggled, + operationName, + rpc, + serviceName, + showErrorIcon, + ticks, + viewEnd, + viewStart, + } = props; + + const labelDetail = `${serviceName}::${operationName}`; + let longLabel; + let hintSide; + if (viewStart > 1 - viewEnd) { + longLabel = `${labelDetail} | ${label}`; + hintSide = 'left'; + } else { + longLabel = `${label} | ${labelDetail}`; + hintSide = 'right'; + } + + let title = serviceName; + if (rpc) { + title += ` → ${rpc.serviceName}::${rpc.operationName}`; + } else { + title += `::${operationName}`; + } + return ( + + +