// Copyright (c) 2017 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import React from 'react'; import _get from 'lodash/get'; import IoChevronRight from 'react-icons/lib/io/chevron-right'; import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; import { css } from 'emotion'; import cx from 'classnames'; import { TraceSpan } from '../types/trace'; import spanAncestorIds from '../utils/span-ancestor-ids'; import { autoColor, createStyle, Theme, withTheme } from '../Theme'; export const getStyles = createStyle((theme: Theme) => { return { SpanTreeOffset: css` label: SpanTreeOffset; color: ${autoColor(theme, '#000')}; position: relative; `, SpanTreeOffsetParent: css` label: SpanTreeOffsetParent; &:hover { background-color: ${autoColor(theme, '#e8e8e8')}; cursor: pointer; } `, indentGuide: css` label: indentGuide; /* The size of the indentGuide is based off of the iconWrapper */ padding-right: calc(0.5rem + 12px); height: 100%; border-left: 3px solid transparent; display: inline-flex; &::before { content: ''; padding-left: 1px; background-color: ${autoColor(theme, 'lightgrey')}; } `, indentGuideActive: css` label: indentGuideActive; border-color: ${autoColor(theme, 'darkgrey')}; &::before { background-color: transparent; } `, iconWrapper: css` label: iconWrapper; position: absolute; right: 0.25rem; `, }; }); type TProps = { childrenVisible?: boolean; onClick?: () => void; span: TraceSpan; showChildrenIcon?: boolean; hoverIndentGuideIds: Set; addHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void; theme: Theme; }; export class UnthemedSpanTreeOffset extends React.PureComponent { static displayName = 'UnthemedSpanTreeOffset'; ancestorIds: string[]; static defaultProps = { childrenVisible: false, showChildrenIcon: true, }; constructor(props: TProps) { super(props); this.ancestorIds = spanAncestorIds(props.span); // 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. this.ancestorIds.push('root'); this.ancestorIds.reverse(); } /** * 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 hoverIndentGuideIds. * * @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: React.MouseEvent, ancestorId: string) => { if ( !(event.relatedTarget instanceof HTMLSpanElement) || _get(event, 'relatedTarget.dataset.ancestorId') !== ancestorId ) { this.props.removeHoverIndentGuideId(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 hoverIndentGuideIds. * * @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: React.MouseEvent, ancestorId: string) => { if ( !(event.relatedTarget instanceof HTMLSpanElement) || _get(event, 'relatedTarget.dataset.ancestorId') !== ancestorId ) { this.props.addHoverIndentGuideId(ancestorId); } }; render() { const { childrenVisible, onClick, showChildrenIcon, span, theme } = this.props; const { hasChildren, spanID } = span; const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null; const icon = showChildrenIcon && hasChildren && (childrenVisible ? : ); const styles = getStyles(theme); return ( {this.ancestorIds.map((ancestorId) => ( this.handleMouseEnter(event, ancestorId)} onMouseLeave={(event) => this.handleMouseLeave(event, ancestorId)} /> ))} {icon && ( this.handleMouseEnter(event, spanID)} onMouseLeave={(event) => this.handleMouseLeave(event, spanID)} data-test-id="icon-wrapper" > {icon} )} ); } } export default withTheme(UnthemedSpanTreeOffset);