diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBar.css b/src/components/TracePage/TraceTimelineViewer/SpanBar.css index 5d9cc54d1a..1761b430e1 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBar.css +++ b/src/components/TracePage/TraceTimelineViewer/SpanBar.css @@ -37,7 +37,6 @@ THE SOFTWARE. opacity: 1; } -/* Add the hint related selector to override the hint styling (via specificity) */ .SpanBar--bar { border-radius: 3px; min-width: 2px; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBar.js b/src/components/TracePage/TraceTimelineViewer/SpanBar.js index 49847ad63b..4cd47544bb 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBar.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanBar.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,17 +20,32 @@ // 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 './SpanBar.css'; -function toPercent(value) { +type SpanBarProps = { + color: string, + hintSide: string, + label: string, + onClick: (SyntheticMouseEvent) => void, + viewEnd: number, + viewStart: number, + rpc: { + viewStart: number, + viewEnd: number, + color: string, + }, + setLongLabel: () => void, + setShortLabel: () => void, +}; + +function toPercent(value: number) { return `${value * 100}%`; } -function SpanBar(props) { +function SpanBar(props: SpanBarProps) { const { viewEnd, viewStart, color, label, hintSide, onClick, setLongLabel, setShortLabel, rpc } = props; return ( @@ -59,29 +76,6 @@ function SpanBar(props) { ); } -SpanBar.propTypes = { - rpc: PropTypes.shape({ - viewStart: PropTypes.number, - viewEnd: PropTypes.number, - color: 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, - setLongLabel: PropTypes.func, - setShortLabel: PropTypes.func, -}; - -SpanBar.defaultProps = { - rpc: null, - onClick: null, - onMouseOver: null, - onMouseOut: null, -}; - export default compose( withState('label', 'setLabel', props => props.shortLabel), withProps(({ setLabel, shortLabel, longLabel }) => ({ diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css index 0c3bbd4595..972a9fab27 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css +++ b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css @@ -43,12 +43,12 @@ THE SOFTWARE. .span-name-wrapper { background: #fafafa; - border-right: 1px solid #bbb; overflow: hidden; text-overflow: ellipsis; } .span-name-wrapper:hover { + border-right: 1px solid #bbb; float: left; min-width: 100%; overflow: visible; @@ -79,7 +79,6 @@ THE SOFTWARE. .span-name:hover > .endpoint-name { color: #000; - font-weight: bold; } .span-svc-name { diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js index 656d6dcb58..5ccae96224 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.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 @@ -19,7 +21,6 @@ // THE SOFTWARE. import React from 'react'; -import PropTypes from 'prop-types'; import TimelineRow from './TimelineRow'; import SpanTreeOffset from './SpanTreeOffset'; @@ -28,10 +29,38 @@ import Ticks from './Ticks'; import './SpanBarRow.css'; -export default function SpanBarRow(props) { +type SpanBarRowProps = { + className: string, + color: string, + columnDivision: number, + depth: number, + isChildrenExpanded: boolean, + isDetailExapnded: boolean, + isFilteredOut: boolean, + isParent: boolean, + label: string, + onDetailToggled: () => void, + onChildrenToggled: () => void, + operationName: string, + numTicks: number, + rpc: ?{ + viewStart: number, + viewEnd: number, + color: string, + operationName: string, + serviceName: string, + }, + serviceName: string, + showErrorIcon: boolean, + viewEnd: number, + viewStart: number, +}; + +export default function SpanBarRow(props: SpanBarRowProps) { const { className, color, + columnDivision, depth, isChildrenExpanded, isDetailExapnded, @@ -40,11 +69,11 @@ export default function SpanBarRow(props) { label, onDetailToggled, onChildrenToggled, + numTicks, operationName, rpc, serviceName, showErrorIcon, - ticks, viewEnd, viewStart, } = props; @@ -68,7 +97,7 @@ export default function SpanBarRow(props) { ${isFilteredOut ? 'is-filtered-out' : ''} `} > - +
-
- - + + + - + ); } -SpanBarRow.propTypes = { - className: PropTypes.string, - color: PropTypes.string.isRequired, - depth: PropTypes.number.isRequired, - isChildrenExpanded: PropTypes.bool.isRequired, - isDetailExapnded: PropTypes.bool.isRequired, - isFilteredOut: PropTypes.bool.isRequired, - isParent: PropTypes.bool.isRequired, - label: PropTypes.string.isRequired, - onDetailToggled: PropTypes.func.isRequired, - onChildrenToggled: PropTypes.func.isRequired, - operationName: PropTypes.string.isRequired, - rpc: PropTypes.shape({ - viewStart: PropTypes.number, - viewEnd: PropTypes.number, - color: PropTypes.string, - operationName: PropTypes.string, - serviceName: PropTypes.string, - }), - serviceName: PropTypes.string.isRequired, - showErrorIcon: PropTypes.bool.isRequired, - ticks: PropTypes.arrayOf(PropTypes.number).isRequired, - viewEnd: PropTypes.number.isRequired, - viewStart: PropTypes.number.isRequired, -}; - SpanBarRow.defaultProps = { className: '', rpc: null, diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.css b/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.css index 2ba9299638..3e84809b98 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.css +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.css @@ -20,15 +20,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - -.detail-row-name-column { - border-right: 1px solid #bbb; -} - .detail-row-expanded-accent { cursor: pointer; - position: absolute; height: 100%; + overflow: hidden; + position: absolute; width: 100%; } @@ -62,7 +58,7 @@ THE SOFTWARE. .detail-info-wrapper { background: #f5f5f5; border: 1px solid #d3d3d3; - border-left: none; + border-left: 1px solid #bbb; border-top: 3px solid; box-shadow: inset 0 16px 20px -20px rgba(0,0,0,0.45), diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js b/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js index fd1c97e4f5..2e5638a849 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js @@ -32,6 +32,7 @@ import './SpanDetailRow.css'; type SpanDetailRowProps = { color: string, + columnDivision: number, detailState: DetailState, detailToggle: string => void, isFilteredOut: boolean, @@ -46,6 +47,7 @@ type SpanDetailRowProps = { export default function SpanDetailRow(props: SpanDetailRowProps) { const { color, + columnDivision, detailState, detailToggle, isFilteredOut, @@ -58,7 +60,7 @@ export default function SpanDetailRow(props: SpanDetailRowProps) { } = props; return ( - + - - + +
-
+
); } diff --git a/src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.js b/src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.js index e0593c0c86..0da6823eb1 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.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,19 @@ // 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 './SpanTreeOffset.css'; -export default function SpanTreeOffset({ level, hasChildren, childrenVisible, onClick }) { +type SpanTreeOffsetProps = { + level: number, + hasChildren: boolean, + childrenVisible: boolean, + onClick: ?() => void, +}; + +export default function SpanTreeOffset(props: SpanTreeOffsetProps) { + const { level, hasChildren, childrenVisible, onClick } = props; const className = hasChildren ? 'span-kids-toggle' : ''; const icon = hasChildren ? @@ -36,13 +45,6 @@ export default function SpanTreeOffset({ level, hasChildren, childrenVisible, on ); } -SpanTreeOffset.propTypes = { - level: PropTypes.number.isRequired, - hasChildren: PropTypes.bool, - childrenVisible: PropTypes.bool, - onClick: PropTypes.func, -}; - SpanTreeOffset.defaultProps = { hasChildren: false, childrenVisible: false, diff --git a/src/components/TracePage/TraceTimelineViewer/Ticks.js b/src/components/TracePage/TraceTimelineViewer/Ticks.js index 943fa7770e..8188a88f11 100644 --- a/src/components/TracePage/TraceTimelineViewer/Ticks.js +++ b/src/components/TracePage/TraceTimelineViewer/Ticks.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,42 +20,52 @@ // 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 * as React from 'react'; + +import { formatDuration } from './utils'; import './Ticks.css'; -export default function Ticks(props) { - const { labels, ticks } = props; +type TicksProps = { + endTime?: number, + numTicks: number, + showLabels?: boolean, + startTime?: ?number, +}; + +export default function Ticks(props: TicksProps) { + const { endTime, numTicks, showLabels, startTime } = props; + let labels: string[]; + if (showLabels) { + labels = []; + const viewingDuration = (endTime || 0) - (startTime || 0); + for (let i = 0; i < numTicks; i++) { + const durationAtTick = startTime + i / (numTicks - 1) * viewingDuration; + labels.push(formatDuration(durationAtTick)); + } + } + const ticks: React.Node[] = []; + for (let i = 0; i < numTicks; i++) { + const portion = i / (numTicks - 1); + ticks.push( +
+ {labels && + = 1 ? 'is-end-anchor' : ''}`}> + {labels[i]} + } +
+ ); + } return (
- {ticks.map( - (tick, i) => - i - ?
- {labels && - = 1 ? 'is-end-anchor' : ''}`}> - {labels[i]} - } -
- : null - )} + {ticks}
); } - -Ticks.propTypes = { - ticks: PropTypes.arrayOf(PropTypes.number).isRequired, - labels: PropTypes.arrayOf(PropTypes.string), -}; - -Ticks.defaultProps = { - labels: null, -}; diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.css b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.css new file mode 100644 index 0000000000..b8a69fa2f9 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.css @@ -0,0 +1,129 @@ +/* +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. +*/ + +.TimelineColumnResizer { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.TimelineColumnResizer--wrapper { + bottom: 0; + position: absolute; + top: 0; +} + +.TimelineColumnResizer--dragger { + border-left: 1px solid transparent; + bottom: 0; + cursor: ew-resize; + position: fixed; + top: 0; + width: 5px; +} + +.TimelineColumnResizer--dragger:hover { + border-left: 1px solid #999; +} + +.TimelineColumnResizer--wrapper.isDraggingLeft > .TimelineColumnResizer--dragger, +.TimelineColumnResizer--wrapper.isDraggingRight > .TimelineColumnResizer--dragger { + /* #808 === rgb(136, 0, 136) */ + background: rgba(136, 0, 136, 0.05); + width: auto; +} + +.TimelineColumnResizer--wrapper.isDraggingLeft > .TimelineColumnResizer--dragger { + border-left: 2px solid #808; + border-right: 1px solid #999; +} + +.TimelineColumnResizer--wrapper.isDraggingRight > .TimelineColumnResizer--dragger { + border-left: 1px solid #999; + border-right: 2px solid #808; +} + +.TimelineColumnResizer--dragger::before { + position: absolute; + top: 0; + bottom: 0; + left: -8px; + right: -5px; + content: " "; +} + +.TimelineColumnResizer--wrapper.isDraggingLeft > .TimelineColumnResizer--dragger::before, +.TimelineColumnResizer--wrapper.isDraggingRight > .TimelineColumnResizer--dragger::before { + left: -2000px; + right: -2000px; +} + +.TimelineColumnResizer--gripIcon { + position: absolute; + top: 0; + bottom: 0; +} + +.TimelineColumnResizer--gripIcon::before, +.TimelineColumnResizer--gripIcon::after { + position: absolute; + content: " "; + top: 60%; + bottom: 10%; + right: 8px; + border-right: 1px solid #ccc; +} + +.TimelineColumnResizer--gripIcon::after { + right: 4px; +} + +.TimelineColumnResizer--wrapper:hover > .TimelineColumnResizer--gripIcon::before, +.TimelineColumnResizer--wrapper:hover > .TimelineColumnResizer--gripIcon::after { + border-right: 1px solid #999; +} +.TimelineColumnResizer--wrapper.isDraggingLeft > .TimelineColumnResizer--gripIcon::before, +.TimelineColumnResizer--wrapper.isDraggingRight > .TimelineColumnResizer--gripIcon::before, +.TimelineColumnResizer--wrapper.isDraggingLeft > .TimelineColumnResizer--gripIcon::after, +.TimelineColumnResizer--wrapper.isDraggingRight > .TimelineColumnResizer--gripIcon::after { + border-right: 1px solid rgba(136, 0, 136, 0.5); +} + +.TimelineHeaderRow { + background: #e8e8e8; + border-bottom: 1px solid #ccc; + height: 38px; + overflow: hidden; + position: fixed; + width: 100%; + z-index: 2; +} + +.TimelineHeaderRow--title { + overflow: hidden; + padding: 0.5rem; + padding-right: 0.1rem; + text-overflow: ellipsis; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.js b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.js new file mode 100644 index 0000000000..3fe0e5a013 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow.js @@ -0,0 +1,197 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import * as React from 'react'; +import cx from 'classnames'; +import _clamp from 'lodash/clamp'; +import _get from 'lodash/get'; + +import Ticks from './Ticks'; +import TimelineRow from './TimelineRow'; + +import './TimelineHeaderRow.css'; + +type TimelineHeaderRowProps = { + endTime: number, + nameColumnWidth: number, + numTicks: number, + onColummWidthChange: number => void, + startTime: number, +}; + +type TimelineColumnResizerProps = { + min: number, + max: number, + onChange: number => void, + position: number, +}; + +type TimelineColumnResizerState = { + dragPosition: ?number, + rootX: ?number, +}; + +const LEFT_MOUSE_BUTTON = 0; + +class TimelineColumnResizer extends React.PureComponent< + TimelineColumnResizerProps, + TimelineColumnResizerState +> { + props: TimelineColumnResizerProps; + state: TimelineColumnResizerState; + _rootElm: ?Element; + _isDragging: boolean; + + constructor(props) { + super(props); + this._rootElm = undefined; + this._isDragging = false; + this.state = { + dragPosition: null, + rootX: null, + }; + this._setRootElm = this._setRootElm.bind(this); + this._onDraggerMouseDown = this._onDraggerMouseDown.bind(this); + this._onWindowMouseMove = this._onWindowMouseMove.bind(this); + this._onWindowMouseUp = this._onWindowMouseUp.bind(this); + } + + componentWillUnmount() { + if (this._isDragging) { + window.removeEventListener('mousemove', this._onWindowMouseMove); + window.removeEventListener('mouseup', this._onWindowMouseUp); + this._isDragging = false; + } + } + + _setRootElm = function _setRootElm(elm) { + this._rootElm = elm; + }; + + _getDraggedPosition(clientX: number, rootX: ?number = null) { + if (!this._rootElm) { + return null; + } + const { min, max } = this.props; + const rx = rootX == null ? this.state.rootX : rootX; + // pos is position of cursor in the horizontal portion of the bounding box, + // in range [0, 1] + const pos = (clientX - (rx || 0)) / this._rootElm.clientWidth; + return _clamp(pos, min, max); + } + + _onDraggerMouseDown = function _onDraggerMouseDown({ button, clientX }) { + if (this._isDragging || button !== LEFT_MOUSE_BUTTON || !this._rootElm) { + return; + } + const rootX = this._rootElm.getBoundingClientRect().left; + const dragPosition = this._getDraggedPosition(clientX, rootX); + this.setState({ rootX, dragPosition }); + window.addEventListener('mousemove', this._onWindowMouseMove); + window.addEventListener('mouseup', this._onWindowMouseUp); + this._isDragging = true; + const style = _get(document, 'body.style'); + if (style) { + (style: any).userSelect = 'none'; + } + }; + + _onWindowMouseMove = function _onWindowMouseMove({ clientX }) { + const dragPosition = this._getDraggedPosition(clientX); + this.setState({ ...this.state, dragPosition }); + }; + + _onWindowMouseUp = function _onWindowMouseUp({ clientX }) { + window.removeEventListener('mousemove', this._onWindowMouseMove); + window.removeEventListener('mouseup', this._onWindowMouseUp); + const style = _get(document, 'body.style'); + if (style) { + (style: any).userSelect = undefined; + } + this._isDragging = false; + const dragPosition = this._getDraggedPosition(clientX); + if (dragPosition != null) { + this.props.onChange(dragPosition); + } + this.setState({ rootX: null, dragPosition: null }); + }; + + render() { + let left; + let draggerStyle; + let draggerStateCls = ''; + const { dragPosition } = this.state; + if (this._isDragging && this._rootElm && dragPosition != null) { + const { position } = this.props; + draggerStateCls = cx({ + isDraggingLeft: dragPosition < position, + isDraggingRight: dragPosition > position, + }); + left = `${dragPosition * 100}%`; + // Draw a highlight from the current dragged position back to the original + // position, e.g. highlight the change. Draw the highlight via `left` and + // `right` css styles (simpler than using `width`). + const draggerLeft = `${Math.min(position, dragPosition) * 100}%`; + // subtract 1px for draggerRight to deal with the right border being off + // by 1px when dragging left + const draggerRight = `calc(${(1 - Math.max(position, dragPosition)) * 100}% - 1px)`; + draggerStyle = { left: draggerLeft, right: draggerRight }; + } else { + const { position } = this.props; + left = `${position * 100}%`; + draggerStyle = { left }; + } + return ( +
+
+
+
+
+
+ ); + } +} + +export default function TimelineHeaderRow(props: TimelineHeaderRowProps) { + const { endTime, nameColumnWidth, numTicks, onColummWidthChange, startTime } = props; + return ( + + +

Service & Operation

+
+ + + + +
+ ); +} diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineRow.js b/src/components/TracePage/TraceTimelineViewer/TimelineRow.js index e64b9af94d..80519f11ea 100644 --- a/src/components/TracePage/TraceTimelineViewer/TimelineRow.js +++ b/src/components/TracePage/TraceTimelineViewer/TimelineRow.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,22 +20,23 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import './TimelineRow.css'; -const propTypes = { - children: PropTypes.node, - className: PropTypes.string, +type TimelineRowProps = { + children?: React.Node, + className: string, }; -const defaultProps = { - children: null, - className: '', +type TimelineRowCellProps = { + children?: React.Node, + className: string, + width: number, + style?: Object, }; -export default function TimelineRow(props) { +export default function TimelineRow(props: TimelineRowProps) { const { children, className, ...rest } = props; return (
@@ -41,31 +44,22 @@ export default function TimelineRow(props) {
); } -TimelineRow.propTypes = { ...propTypes }; -TimelineRow.defaultProps = { ...defaultProps }; -function TimelineRowLeft(props) { - const { children, className, ...rest } = props; - return ( -
- {children} -
- ); -} -TimelineRowLeft.propTypes = { ...propTypes }; -TimelineRowLeft.defaultProps = { ...defaultProps }; - -TimelineRow.Left = TimelineRowLeft; +TimelineRow.defaultProps = { + className: '', +}; -function TimelineRowRight(props) { - const { children, className, ...rest } = props; +function TimelineRowCell(props: TimelineRowCellProps) { + const { children, className, width, style, ...rest } = props; + const widthPercent = `${width * 100}%`; + const mergedStyle = { ...style, flexBasis: widthPercent, maxWidth: widthPercent }; return ( -
+
{children}
); } -TimelineRowRight.propTypes = { ...propTypes }; -TimelineRowRight.defaultProps = { ...defaultProps }; -TimelineRow.Right = TimelineRowRight; +TimelineRowCell.defaultProps = { className: '' }; + +TimelineRow.Cell = TimelineRowCell; diff --git a/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.css b/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.css index 8859d09f8e..9790ee2688 100644 --- a/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.css +++ b/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.css @@ -20,20 +20,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -.VirtualizedTraceView--headerRow { - background: #e8e8e8; - border-bottom: 1px solid #ccc; - height: 38px; - overflow: hidden; - position: fixed; - width: 100%; - z-index: 2; -} - -.VirtualizedTraceView--labelHeader { - border-right: 1px solid #bbb; -} - .VirtualizedTraceView--spans { padding-top: 40px; position: relative; diff --git a/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js b/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js index 152f9076a7..eb766d6765 100644 --- a/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js +++ b/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js @@ -30,8 +30,7 @@ import ListView from './ListView'; import SpanBarRow from './SpanBarRow'; import DetailState from './SpanDetail/DetailState'; import SpanDetailRow from './SpanDetailRow'; -import Ticks from './Ticks'; -import TimelineRow from './TimelineRow'; +import TimelineHeaderRow from './TimelineHeaderRow'; import { findServerChildSpan, formatDuration, @@ -62,8 +61,9 @@ type VirtualizedTraceViewProps = { find: (?Trace, ?string) => void, findMatchesIDs: Set, setTrace: (?string) => void, + setSpanNameColumnWidth: number => void, + spanNameColumnWidth: number, textFilter: ?string, - ticks: number[], trace?: Trace, zoomEnd: number, zoomStart: number, @@ -75,6 +75,8 @@ const DEFAULT_HEIGHTS = { detailWithLogs: 223, }; +const NUM_TICKS = 5; + function generateRowStates( spans: ?(Span[]), childrenHiddenIDs: Set, @@ -162,9 +164,15 @@ class VirtualizedTraceView extends React.PureComponent childrenToggle(spanID)} operationName={span.operationName} rpc={rpc} serviceName={span.process.serviceName} showErrorIcon={showErrorIcon} - ticks={ticks} viewEnd={viewBounds.end} viewStart={viewBounds.start} /> @@ -303,6 +312,7 @@ class VirtualizedTraceView extends React.PureComponent detailToggle(spanID)} detailState={detailState} isFilteredOut={isFilteredOut} @@ -330,26 +341,21 @@ class VirtualizedTraceView extends React.PureComponent - - -

Service & Operation

-
- - formatDuration(getDuationAtTick(tick)))} ticks={ticks} /> - -
+
, // findMatches: ?Set, // detailStates: Map // } -// -export function newInitialState(traceID = null) { +export function newInitialState({ spanNameColumnWidth = null, traceID = null } = {}) { return { traceID, + spanNameColumnWidth: spanNameColumnWidth || 0.25, childrenHiddenIDs: new Set(), detailStates: new Map(), findMatchesIDs: null, @@ -52,6 +53,7 @@ export function newInitialState(traceID = null) { const actionTypes = generateActionTypes('@jaeger-ui/trace-timeline-viewer', [ 'SET_TRACE', + 'SET_SPAN_NAME_COLUMN_WIDTH', 'CHILDREN_TOGGLE', 'DETAIL_TOGGLE', 'DETAIL_TAGS_TOGGLE', @@ -63,6 +65,7 @@ const actionTypes = generateActionTypes('@jaeger-ui/trace-timeline-viewer', [ const fullActions = createActions({ [actionTypes.SET_TRACE]: traceID => ({ traceID }), + [actionTypes.SET_SPAN_NAME_COLUMN_WIDTH]: width => ({ width }), [actionTypes.CHILDREN_TOGGLE]: spanID => ({ spanID }), [actionTypes.DETAIL_TOGGLE]: spanID => ({ spanID }), [actionTypes.DETAIL_TAGS_TOGGLE]: spanID => ({ spanID }), @@ -79,7 +82,14 @@ function setTrace(state, { payload }) { if (traceID === state.traceID) { return state; } - return newInitialState(traceID); + // preserve spanNameColumnWidth when resetting state + const { spanNameColumnWidth } = state; + return newInitialState({ spanNameColumnWidth, traceID }); +} + +function setColumnWidth(state, { payload }) { + const { width } = payload; + return { ...state, spanNameColumnWidth: width }; } function childrenToggle(state, { payload }) { @@ -143,6 +153,7 @@ function find(state, { payload }) { export default handleActions( { [actionTypes.SET_TRACE]: setTrace, + [actionTypes.SET_SPAN_NAME_COLUMN_WIDTH]: setColumnWidth, [actionTypes.CHILDREN_TOGGLE]: childrenToggle, [actionTypes.DETAIL_TOGGLE]: detailToggle, [actionTypes.DETAIL_TAGS_TOGGLE]: detailTagsToggle, diff --git a/src/components/TracePage/TraceTimelineViewer/index.css b/src/components/TracePage/TraceTimelineViewer/index.css index ef020eb226..baa520b10a 100644 --- a/src/components/TracePage/TraceTimelineViewer/index.css +++ b/src/components/TracePage/TraceTimelineViewer/index.css @@ -64,646 +64,3 @@ THE SOFTWARE. .json-markup-number { color: blue; } -/*! Hint.css - v2.4.1 - 2016-11-08 -* http://kushagragour.in/lab/hint/ -* Copyright (c) 2016 Kushagra Gour */ - -/*-------------------------------------* HINT.css - A CSS tooltip library -\*-------------------------------------*/ -/** - * HINT.css is a tooltip library made in pure CSS. - * - * Source: https://github.com/chinchang/hint.css - * Demo: http://kushagragour.in/lab/hint/ - * - * Release under The MIT License - * - */ -/** - * source: hint-core.scss - * - * Defines the basic styling for the tooltip. - * Each tooltip is made of 2 parts: - * 1) body (:after) - * 2) arrow (:before) - * - * Classes added: - * 1) hint - */ -[class*="hint--"] { - position: relative; - display: inline-block; - /** - * tooltip arrow - */ - /** - * tooltip body - */ } - [class*="hint--"]:before, [class*="hint--"]:after { - position: absolute; - -webkit-transform: translate3d(0, 0, 0); - -moz-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - visibility: hidden; - opacity: 0; - z-index: 1000000; - pointer-events: none; - -webkit-transition: 0.3s ease; - -moz-transition: 0.3s ease; - transition: 0.3s ease; - -webkit-transition-delay: 0ms; - -moz-transition-delay: 0ms; - transition-delay: 0ms; } - [class*="hint--"]:hover:before, [class*="hint--"]:hover:after { - visibility: visible; - opacity: 1; } - [class*="hint--"]:hover:before, [class*="hint--"]:hover:after { - -webkit-transition-delay: 100ms; - -moz-transition-delay: 100ms; - transition-delay: 100ms; } - [class*="hint--"]:before { - content: ''; - position: absolute; - background: transparent; - z-index: 1000001; } - [class*="hint--"]:after { - color: black; - padding: 8px 0px; - font-size: 12px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - line-height: 12px; - white-space: nowrap; } - [class*="hint--"][aria-label]:after { - content: attr(aria-label); } - [class*="hint--"][data-hint]:after { - content: attr(data-hint); } - -[aria-label='']:before, [aria-label='']:after, -[data-hint='']:before, -[data-hint='']:after { - display: none !important; } - -/** - * source: hint-position.scss - * - * Defines the positoning logic for the tooltips. - * - * Classes added: - * 1) hint--top - * 2) hint--bottom - * 3) hint--left - * 4) hint--right - */ -/** - * set default color for tooltip arrows - */ -.hint--top-left:before { - border-top-color: #383838; } - -.hint--top-right:before { - border-top-color: #383838; } - -.hint--top:before { - border-top-color: #383838; } - -.hint--bottom-left:before { - border-bottom-color: #383838; } - -.hint--bottom-right:before { - border-bottom-color: #383838; } - -.hint--bottom:before { - border-bottom-color: #383838; } - -.hint--left:before { - border-left-color: #383838; } - -.hint--right:before { - border-right-color: #383838; } - -/** - * top tooltip - */ -.hint--top:before { - margin-bottom: -11px; } - -.hint--top:before, .hint--top:after { - bottom: 100%; - left: 50%; } - -.hint--top:before { - left: calc(50% - 6px); } - -.hint--top:after { - -webkit-transform: translateX(-50%); - -moz-transform: translateX(-50%); - transform: translateX(-50%); } - -.hint--top:hover:before { - -webkit-transform: translateY(-8px); - -moz-transform: translateY(-8px); - transform: translateY(-8px); } - -.hint--top:hover:after { - -webkit-transform: translateX(-50%) translateY(-8px); - -moz-transform: translateX(-50%) translateY(-8px); - transform: translateX(-50%) translateY(-8px); } - -/** - * bottom tooltip - */ -.hint--bottom:before { - margin-top: -11px; } - -.hint--bottom:before, .hint--bottom:after { - top: 100%; - left: 50%; } - -.hint--bottom:before { - left: calc(50% - 6px); } - -.hint--bottom:after { - -webkit-transform: translateX(-50%); - -moz-transform: translateX(-50%); - transform: translateX(-50%); } - -.hint--bottom:hover:before { - -webkit-transform: translateY(8px); - -moz-transform: translateY(8px); - transform: translateY(8px); } - -.hint--bottom:hover:after { - -webkit-transform: translateX(-50%) translateY(8px); - -moz-transform: translateX(-50%) translateY(8px); - transform: translateX(-50%) translateY(8px); } - -/** - * right tooltip - */ -.hint--right:before { - margin-left: -11px; - margin-bottom: -6px; } - -.hint--right:after { - margin-bottom: -14px; } - -.hint--right:before, .hint--right:after { - left: 100%; - bottom: 50%; } - -.hint--right:hover:before { - -webkit-transform: translateX(8px); - -moz-transform: translateX(8px); - transform: translateX(8px); } - -.hint--right:hover:after { - -webkit-transform: translateX(8px); - -moz-transform: translateX(8px); - transform: translateX(8px); } - -/** - * left tooltip - */ -.hint--left:before { - margin-right: -11px; - margin-bottom: -6px; } - -.hint--left:after { - margin-bottom: -14px; } - -.hint--left:before, .hint--left:after { - right: 100%; - bottom: 50%; } - -.hint--left:hover:before { - -webkit-transform: translateX(-8px); - -moz-transform: translateX(-8px); - transform: translateX(-8px); } - -.hint--left:hover:after { - -webkit-transform: translateX(-8px); - -moz-transform: translateX(-8px); - transform: translateX(-8px); } - -/** - * top-left tooltip - */ -.hint--top-left:before { - margin-bottom: -11px; } - -.hint--top-left:before, .hint--top-left:after { - bottom: 100%; - left: 50%; } - -.hint--top-left:before { - left: calc(50% - 6px); } - -.hint--top-left:after { - -webkit-transform: translateX(-100%); - -moz-transform: translateX(-100%); - transform: translateX(-100%); } - -.hint--top-left:after { - margin-left: 12px; } - -.hint--top-left:hover:before { - -webkit-transform: translateY(-8px); - -moz-transform: translateY(-8px); - transform: translateY(-8px); } - -.hint--top-left:hover:after { - -webkit-transform: translateX(-100%) translateY(-8px); - -moz-transform: translateX(-100%) translateY(-8px); - transform: translateX(-100%) translateY(-8px); } - -/** - * top-right tooltip - */ -.hint--top-right:before { - margin-bottom: -11px; } - -.hint--top-right:before, .hint--top-right:after { - bottom: 100%; - left: 50%; } - -.hint--top-right:before { - left: calc(50% - 6px); } - -.hint--top-right:after { - -webkit-transform: translateX(0); - -moz-transform: translateX(0); - transform: translateX(0); } - -.hint--top-right:after { - margin-left: -12px; } - -.hint--top-right:hover:before { - -webkit-transform: translateY(-8px); - -moz-transform: translateY(-8px); - transform: translateY(-8px); } - -.hint--top-right:hover:after { - -webkit-transform: translateY(-8px); - -moz-transform: translateY(-8px); - transform: translateY(-8px); } - -/** - * bottom-left tooltip - */ -.hint--bottom-left:before { - margin-top: -11px; } - -.hint--bottom-left:before, .hint--bottom-left:after { - top: 100%; - left: 50%; } - -.hint--bottom-left:before { - left: calc(50% - 6px); } - -.hint--bottom-left:after { - -webkit-transform: translateX(-100%); - -moz-transform: translateX(-100%); - transform: translateX(-100%); } - -.hint--bottom-left:after { - margin-left: 12px; } - -.hint--bottom-left:hover:before { - -webkit-transform: translateY(8px); - -moz-transform: translateY(8px); - transform: translateY(8px); } - -.hint--bottom-left:hover:after { - -webkit-transform: translateX(-100%) translateY(8px); - -moz-transform: translateX(-100%) translateY(8px); - transform: translateX(-100%) translateY(8px); } - -/** - * bottom-right tooltip - */ -.hint--bottom-right:before { - margin-top: -11px; } - -.hint--bottom-right:before, .hint--bottom-right:after { - top: 100%; - left: 50%; } - -.hint--bottom-right:before { - left: calc(50% - 6px); } - -.hint--bottom-right:after { - -webkit-transform: translateX(0); - -moz-transform: translateX(0); - transform: translateX(0); } - -.hint--bottom-right:after { - margin-left: -12px; } - -.hint--bottom-right:hover:before { - -webkit-transform: translateY(8px); - -moz-transform: translateY(8px); - transform: translateY(8px); } - -.hint--bottom-right:hover:after { - -webkit-transform: translateY(8px); - -moz-transform: translateY(8px); - transform: translateY(8px); } - -/** - * source: hint-sizes.scss - * - * Defines width restricted tooltips that can span - * across multiple lines. - * - * Classes added: - * 1) hint--small - * 2) hint--medium - * 3) hint--large - * - */ -.hint--small:after, -.hint--medium:after, -.hint--large:after { - white-space: normal; - line-height: 1.4em; - word-wrap: break-word; } - -.hint--small:after { - width: 80px; } - -.hint--medium:after { - width: 150px; } - -.hint--large:after { - width: 300px; } - -/** - * source: hint-theme.scss - * - * Defines basic theme for tooltips. - * - */ -[class*="hint--"] { - /** - * tooltip body - */ } -/** - * source: hint-color-types.scss - * - * Contains tooltips of various types based on color differences. - * - * Classes added: - * 1) hint--error - * 2) hint--warning - * 3) hint--info - * 4) hint--success - * - */ -/** - * Error - */ -.hint--error:after { - background-color: #b34e4d; - text-shadow: 0 -1px 0px #592726; } - -.hint--error.hint--top-left:before { - border-top-color: #b34e4d; } - -.hint--error.hint--top-right:before { - border-top-color: #b34e4d; } - -.hint--error.hint--top:before { - border-top-color: #b34e4d; } - -.hint--error.hint--bottom-left:before { - border-bottom-color: #b34e4d; } - -.hint--error.hint--bottom-right:before { - border-bottom-color: #b34e4d; } - -.hint--error.hint--bottom:before { - border-bottom-color: #b34e4d; } - -.hint--error.hint--left:before { - border-left-color: #b34e4d; } - -.hint--error.hint--right:before { - border-right-color: #b34e4d; } - -/** - * Warning - */ -.hint--warning:after { - background-color: #c09854; - text-shadow: 0 -1px 0px #6c5328; } - -.hint--warning.hint--top-left:before { - border-top-color: #c09854; } - -.hint--warning.hint--top-right:before { - border-top-color: #c09854; } - -.hint--warning.hint--top:before { - border-top-color: #c09854; } - -.hint--warning.hint--bottom-left:before { - border-bottom-color: #c09854; } - -.hint--warning.hint--bottom-right:before { - border-bottom-color: #c09854; } - -.hint--warning.hint--bottom:before { - border-bottom-color: #c09854; } - -.hint--warning.hint--left:before { - border-left-color: #c09854; } - -.hint--warning.hint--right:before { - border-right-color: #c09854; } - -/** - * Info - */ -.hint--info:after { - background-color: #3986ac; - text-shadow: 0 -1px 0px #1a3c4d; } - -.hint--info.hint--top-left:before { - border-top-color: #3986ac; } - -.hint--info.hint--top-right:before { - border-top-color: #3986ac; } - -.hint--info.hint--top:before { - border-top-color: #3986ac; } - -.hint--info.hint--bottom-left:before { - border-bottom-color: #3986ac; } - -.hint--info.hint--bottom-right:before { - border-bottom-color: #3986ac; } - -.hint--info.hint--bottom:before { - border-bottom-color: #3986ac; } - -.hint--info.hint--left:before { - border-left-color: #3986ac; } - -.hint--info.hint--right:before { - border-right-color: #3986ac; } - -/** - * Success - */ -.hint--success:after { - background-color: #458746; - text-shadow: 0 -1px 0px #1a321a; } - -.hint--success.hint--top-left:before { - border-top-color: #458746; } - -.hint--success.hint--top-right:before { - border-top-color: #458746; } - -.hint--success.hint--top:before { - border-top-color: #458746; } - -.hint--success.hint--bottom-left:before { - border-bottom-color: #458746; } - -.hint--success.hint--bottom-right:before { - border-bottom-color: #458746; } - -.hint--success.hint--bottom:before { - border-bottom-color: #458746; } - -.hint--success.hint--left:before { - border-left-color: #458746; } - -.hint--success.hint--right:before { - border-right-color: #458746; } - -/** - * source: hint-always.scss - * - * Defines a persisted tooltip which shows always. - * - * Classes added: - * 1) hint--always - * - */ -.hint--always:after, .hint--always:before { - opacity: 1; - visibility: visible; } - -.hint--always.hint--top:before { - -webkit-transform: translateY(-8px); - -moz-transform: translateY(-8px); - transform: translateY(-8px); } - -.hint--always.hint--top:after { - -webkit-transform: translateX(-50%) translateY(-8px); - -moz-transform: translateX(-50%) translateY(-8px); - transform: translateX(-50%) translateY(-8px); } - -.hint--always.hint--top-left:before { - -webkit-transform: translateY(-8px); - -moz-transform: translateY(-8px); - transform: translateY(-8px); } - -.hint--always.hint--top-left:after { - -webkit-transform: translateX(-100%) translateY(-8px); - -moz-transform: translateX(-100%) translateY(-8px); - transform: translateX(-100%) translateY(-8px); } - -.hint--always.hint--top-right:before { - -webkit-transform: translateY(-8px); - -moz-transform: translateY(-8px); - transform: translateY(-8px); } - -.hint--always.hint--top-right:after { - -webkit-transform: translateY(-8px); - -moz-transform: translateY(-8px); - transform: translateY(-8px); } - -.hint--always.hint--bottom:before { - -webkit-transform: translateY(8px); - -moz-transform: translateY(8px); - transform: translateY(8px); } - -.hint--always.hint--bottom:after { - -webkit-transform: translateX(-50%) translateY(8px); - -moz-transform: translateX(-50%) translateY(8px); - transform: translateX(-50%) translateY(8px); } - -.hint--always.hint--bottom-left:before { - -webkit-transform: translateY(8px); - -moz-transform: translateY(8px); - transform: translateY(8px); } - -.hint--always.hint--bottom-left:after { - -webkit-transform: translateX(-100%) translateY(8px); - -moz-transform: translateX(-100%) translateY(8px); - transform: translateX(-100%) translateY(8px); } - -.hint--always.hint--bottom-right:before { - -webkit-transform: translateY(8px); - -moz-transform: translateY(8px); - transform: translateY(8px); } - -.hint--always.hint--bottom-right:after { - -webkit-transform: translateY(8px); - -moz-transform: translateY(8px); - transform: translateY(8px); } - -.hint--always.hint--left:before { - -webkit-transform: translateX(-8px); - -moz-transform: translateX(-8px); - transform: translateX(-8px); } - -.hint--always.hint--left:after { - -webkit-transform: translateX(-8px); - -moz-transform: translateX(-8px); - transform: translateX(-8px); } - -.hint--always.hint--right:before { - -webkit-transform: translateX(8px); - -moz-transform: translateX(8px); - transform: translateX(8px); } - -.hint--always.hint--right:after { - -webkit-transform: translateX(8px); - -moz-transform: translateX(8px); - transform: translateX(8px); } - -/** - * source: hint-rounded.scss - * - * Defines rounded corner tooltips. - * - * Classes added: - * 1) hint--rounded - * - */ -.hint--rounded:after { - border-radius: 4px; } - -/** - * source: hint-effects.scss - * - * Defines various transition effects for the tooltips. - * - * Classes added: - * 1) hint--no-animate - * 2) hint--bounce - * - */ -.hint--no-animate:before, .hint--no-animate:after { - -webkit-transition-duration: 0ms; - -moz-transition-duration: 0ms; - transition-duration: 0ms; } - -.hint--bounce:before, .hint--bounce:after { - -webkit-transition: opacity 0.3s ease, visibility 0.3s ease, -webkit-transform 0.3s cubic-bezier(0.71, 1.7, 0.77, 1.24); - -moz-transition: opacity 0.3s ease, visibility 0.3s ease, -moz-transform 0.3s cubic-bezier(0.71, 1.7, 0.77, 1.24); - transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s cubic-bezier(0.71, 1.7, 0.77, 1.24); } diff --git a/src/components/TracePage/TraceTimelineViewer/index.js b/src/components/TracePage/TraceTimelineViewer/index.js index 3ea28a3b87..1b3d4dd60a 100644 --- a/src/components/TracePage/TraceTimelineViewer/index.js +++ b/src/components/TracePage/TraceTimelineViewer/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,38 +20,30 @@ // 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 React from 'react'; import VirtualizedTraceView from './VirtualizedTraceView'; +import type { Trace } from '../../../types'; import './grid.css'; import './index.css'; -export default class TraceTimelineViewer extends Component { - componentWillReceiveProps(nextProps) { - const { trace } = nextProps; - if (trace !== this.props.trace) { - throw new Error('Component does not support changing the trace'); - } - } +type TraceTimelineViewerProps = { + trace: ?Trace, + timeRangeFilter: [number, number], + textFilter: ?string, +}; - render() { - const { timeRangeFilter: zoomRange, textFilter, trace } = this.props; - return ( -
- -
- ); - } +export default function TraceTimelineViewer(props: TraceTimelineViewerProps) { + const { timeRangeFilter: zoomRange, textFilter, trace } = props; + return ( +
+ +
+ ); } -TraceTimelineViewer.propTypes = { - trace: PropTypes.object, - timeRangeFilter: PropTypes.array, - textFilter: PropTypes.string, -}; diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index 035727f37a..680933963e 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -137,10 +137,17 @@ export default class TracePage extends Component { } render() { - const { id, trace } = this.props; + const { id, loading, trace } = this.props; const { slimView, headerHeight } = this.state; if (!trace) { + if (loading) { + return ( +
+
+
+ ); + } return
; } diff --git a/src/components/TracePage/index.test.js b/src/components/TracePage/index.test.js index 1e9fe7a101..8d484f764d 100644 --- a/src/components/TracePage/index.test.js +++ b/src/components/TracePage/index.test.js @@ -56,6 +56,12 @@ describe('', () => { expect(isEmpty).toBe(true); }); + it('renders a loading indicator when loading', () => { + wrapper = shallow(); + const loading = wrapper.find('.loader'); + expect(loading.length).toBe(1); + }); + // can't do mount tests in standard tape run. it('fetches the trace if necessary', () => { const fetchTrace = sinon.spy();