Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add indent guides to trace timeline view (#172) #297

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import IoAlert from 'react-icons/lib/io/alert';
import IoArrowRightA from 'react-icons/lib/io/arrow-right-a';

import TimelineRow from './TimelineRow';
import type { Span } from '../../../types/trace';
import SpanTreeOffset from './SpanTreeOffset';
import SpanBar from './SpanBar';
import Ticks from './Ticks';
Expand Down Expand Up @@ -48,6 +49,7 @@ type SpanBarRowProps = {
},
serviceName: string,
showErrorIcon: boolean,
span: Span,
everett980 marked this conversation as resolved.
Show resolved Hide resolved
spanID: string,
viewEnd: number,
viewStart: number,
Expand Down Expand Up @@ -93,6 +95,7 @@ export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> {
rpc,
serviceName,
showErrorIcon,
span,
viewEnd,
viewStart,
} = this.props;
Expand Down Expand Up @@ -122,6 +125,7 @@ export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> {
level={depth + 1}
hasChildren={isParent}
childrenVisible={isChildrenExpanded}
span={span}
onClick={isParent ? this._childrenToggle : null}
/>
<a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import React from 'react';
import { mount } from 'enzyme';

import SpanBarRow from './SpanBarRow';
import SpanTreeOffset from './SpanTreeOffset';

jest.mock('./SpanTreeOffset');

describe('<SpanBarRow>', () => {
const spanID = 'some-id';
Expand Down Expand Up @@ -69,7 +72,7 @@ describe('<SpanBarRow>', () => {
it('escalates children toggling', () => {
const { onChildrenToggled } = props;
expect(onChildrenToggled.mock.calls.length).toBe(0);
wrapper.find('SpanTreeOffset').prop('onClick')();
wrapper.find(SpanTreeOffset).prop('onClick')();
expect(onChildrenToggled.mock.calls).toEqual([[spanID]]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default class SpanDetailRow extends React.PureComponent<SpanDetailRowProp
return (
<TimelineRow className={`detail-row`}>
<TimelineRow.Cell width={columnDivision}>
<SpanTreeOffset level={span.depth + 1} />
<SpanTreeOffset span={span} />
<span>
<span
className="detail-row-expanded-accent"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import SpanDetail from './SpanDetail';
import DetailState from './SpanDetail/DetailState';
import SpanTreeOffset from './SpanTreeOffset';

jest.mock('./SpanTreeOffset');
everett980 marked this conversation as resolved.
Show resolved Hide resolved

describe('<SpanDetailRow>', () => {
const spanID = 'some-id';
const props = {
Expand Down Expand Up @@ -61,7 +63,7 @@ describe('<SpanDetailRow>', () => {
});

it('renders the span tree offset', () => {
const spanTreeOffset = <SpanTreeOffset level={props.span.depth + 1} />;
const spanTreeOffset = <SpanTreeOffset span={props.span} />;
expect(wrapper.contains(spanTreeOffset)).toBe(true);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ limitations under the License.
cursor: pointer;
}

.SpanTreeOffset--indentGuide {
/* The size of the indentGuide is based off of the iconWrapper */
padding-right: calc(0.5rem + 12px);
height: 100%;
border-left: 1px solid transparent;
display: inline-flex;
}

.SpanTreeOffset--indentGuide:before {
content: '';
padding-left: 1px;
background-color: lightgrey;
everett980 marked this conversation as resolved.
Show resolved Hide resolved
}

.SpanTreeOffset--indentGuide.activeMouseover {
everett980 marked this conversation as resolved.
Show resolved Hide resolved
/* The size of the indentGuide is based off of the iconWrapper */
padding-right: calc(0.5rem + 11px);
border-left: 0px;
}

.SpanTreeOffset--indentGuide.activeMouseover:before {
content: '';
padding-left: 3px;
background-color: darkgrey;
}

.SpanTreeOffset--iconWrapper {
position: absolute;
right: 0.25rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,150 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import cx from 'classnames';
import _get from 'lodash/get';
import _find from 'lodash/find';
import PropTypes from 'prop-types';
import React from 'react';
import IoChevronRight from 'react-icons/lib/io/chevron-right';
import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import type { ReduxState } from '../../../types/index';
everett980 marked this conversation as resolved.
Show resolved Hide resolved
import type { Span } from '../../../types/trace';
import { actions } from './SpanTreeOffsetDuck';
import './SpanTreeOffset.css';

type SpanTreeOffsetProps = {
level: number,
type SpanTreeOffsetOwnProps = {
addSpanId: string => void,
removeSpanId: string => void,
everett980 marked this conversation as resolved.
Show resolved Hide resolved
hasChildren: boolean,
childrenVisible: boolean,
span: Span,
onClick: ?() => void,
};

export default function SpanTreeOffset(props: SpanTreeOffsetProps) {
const { level, hasChildren, childrenVisible, onClick } = props;
const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null;
const icon = hasChildren && (childrenVisible ? <IoIosArrowDown /> : <IoChevronRight />);
return (
<span className={`SpanTreeOffset ${hasChildren ? 'is-parent' : ''}`} {...wrapperProps}>
<span style={{ paddingLeft: `${level * 20}px` }} />
{icon && <span className="SpanTreeOffset--iconWrapper">{icon}</span>}
</span>
);
type SpanTreeOffsetProps = SpanTreeOffsetOwnProps & {
hoverSpanIds: Set<string>,
};

export class UnconnectedSpanTreeOffset extends React.PureComponent<SpanTreeOffsetProps> {
everett980 marked this conversation as resolved.
Show resolved Hide resolved
ancestorIds: string[];

static propTypes = {
everett980 marked this conversation as resolved.
Show resolved Hide resolved
addSpanId: PropTypes.func.isRequired,
childrenVisible: PropTypes.bool,
hasChildren: PropTypes.bool,
hoverSpanIds: PropTypes.instanceOf(Set).isRequired,
onClick: PropTypes.func,
removeSpanId: PropTypes.func.isRequired,
span: PropTypes.shape({
spanID: PropTypes.string.isRequired,
references: PropTypes.array.isRequired,
}).isRequired,
};

static defaultProps = {
childrenVisible: false,
hasChildren: false,
onClick: null,
};

constructor(props: SpanTreeOffsetProps) {
super(props);

const tempAncestorIds: string[] = [];
let currentSpan: Span = props.span;
while (currentSpan) {
currentSpan = _get(_find(currentSpan.references, { refType: 'CHILD_OF' }), 'span');
if (currentSpan) {
tempAncestorIds.push(currentSpan.spanID);
}
}

// Some traces have multiple root-level spans, this connects them all under one guideline and adds the
// necessary padding for the collapse icon on root-level spans.
tempAncestorIds.push('root');

this.ancestorIds = tempAncestorIds.reverse();
everett980 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* If the mouse leaves to anywhere except another span with the same ancestor id, this span's ancestor id is
* removed from the set of hoverSpanIds.
*
* @param {Object} event - React Synthetic event tied to mouseleave. Includes the related target which is
* the element the user is now hovering.
* @param {string} ancestorId - The span id that the user was hovering over.
*/
handleMouseLeave = (event: any, ancestorId: string) => {
if (
!event.relatedTarget ||
!event.relatedTarget.getAttribute ||
event.relatedTarget.getAttribute('data--ancestor-id') !== ancestorId
everett980 marked this conversation as resolved.
Show resolved Hide resolved
) {
this.props.removeSpanId(ancestorId);
}
};

/**
* If the mouse entered this span from anywhere except another span with the same ancestor id, this span's
* ancestorId is added to the set of hoverSpanIds.
*
* @param {Object} event - React Synthetic event tied to mouseenter. Includes the related target which is
* the last element the user was hovering.
* @param {string} ancestorId - The span id that the user is now hovering over.
*/
handleMouseEnter = (event: any, ancestorId: string) => {
everett980 marked this conversation as resolved.
Show resolved Hide resolved
if (
!event.relatedTarget ||
!event.relatedTarget.getAttribute ||
event.relatedTarget.getAttribute('data--ancestor-id') !== ancestorId
) {
this.props.addSpanId(ancestorId);
}
};

render() {
const { hasChildren, childrenVisible, onClick } = this.props;
const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null;
const icon = hasChildren && (childrenVisible ? <IoIosArrowDown /> : <IoChevronRight />);
return (
<span className={`SpanTreeOffset ${hasChildren ? 'is-parent' : ''}`} {...wrapperProps}>
{this.ancestorIds.map(ancestorId => (
<span
key={ancestorId}
className={cx('SpanTreeOffset--indentGuide', {
activeMouseover: this.props.hoverSpanIds.has(ancestorId),
})}
data--ancestor-id={ancestorId}
onMouseEnter={event => this.handleMouseEnter(event, ancestorId)}
onMouseLeave={event => this.handleMouseLeave(event, ancestorId)}
/>
))}
{icon && (
<span
className="SpanTreeOffset--iconWrapper"
onMouseEnter={event => this.handleMouseEnter(event, this.props.span.spanID)}
onMouseLeave={event => this.handleMouseLeave(event, this.props.span.spanID)}
>
{icon}
</span>
)}
</span>
);
}
}

SpanTreeOffset.defaultProps = {
hasChildren: false,
childrenVisible: false,
onClick: null,
};
export function mapStateToProps(state: ReduxState, ownProps: SpanTreeOffsetOwnProps): SpanTreeOffsetProps {
const hoverSpanIds = state.hoverSpanIds.hoverSpanIds;
return { hoverSpanIds, ...ownProps };
}

export function mapDispatchToProps(dispatch: Function) {
const { addSpanId, removeSpanId } = bindActionCreators(actions, dispatch);
return { addSpanId, removeSpanId };
}

export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedSpanTreeOffset);
Loading