1// Copyright (c) 2017 Uber Technologies, Inc. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import React from 'react'; 16import { get as _get } from 'lodash'; 17import IoChevronRight from 'react-icons/lib/io/chevron-right'; 18import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; 19import { css } from '@emotion/css'; 20import cx from 'classnames'; 21 22import { TraceSpan } from '../types/trace'; 23import spanAncestorIds from '../utils/span-ancestor-ids'; 24 25import { autoColor, createStyle, Theme, withTheme } from '../Theme'; 26 27export const getStyles = createStyle((theme: Theme) => { 28 return { 29 SpanTreeOffset: css` 30 label: SpanTreeOffset; 31 color: ${autoColor(theme, '#000')}; 32 position: relative; 33 `, 34 SpanTreeOffsetParent: css` 35 label: SpanTreeOffsetParent; 36 &:hover { 37 cursor: pointer; 38 } 39 `, 40 indentGuide: css` 41 label: indentGuide; 42 /* The size of the indentGuide is based off of the iconWrapper */ 43 padding-right: calc(0.5rem + 12px); 44 height: 100%; 45 border-left: 3px solid transparent; 46 display: inline-flex; 47 &::before { 48 content: ''; 49 padding-left: 1px; 50 background-color: ${autoColor(theme, 'lightgrey')}; 51 } 52 `, 53 indentGuideActive: css` 54 label: indentGuideActive; 55 border-color: ${autoColor(theme, 'darkgrey')}; 56 &::before { 57 background-color: transparent; 58 } 59 `, 60 iconWrapper: css` 61 label: iconWrapper; 62 position: absolute; 63 right: 0.25rem; 64 `, 65 }; 66}); 67 68type TProps = { 69 childrenVisible?: boolean; 70 onClick?: () => void; 71 span: TraceSpan; 72 showChildrenIcon?: boolean; 73 74 hoverIndentGuideIds: Set<string>; 75 addHoverIndentGuideId: (spanID: string) => void; 76 removeHoverIndentGuideId: (spanID: string) => void; 77 theme: Theme; 78}; 79 80export class UnthemedSpanTreeOffset extends React.PureComponent<TProps> { 81 static displayName = 'UnthemedSpanTreeOffset'; 82 83 ancestorIds: string[]; 84 85 static defaultProps = { 86 childrenVisible: false, 87 showChildrenIcon: true, 88 }; 89 90 constructor(props: TProps) { 91 super(props); 92 93 this.ancestorIds = spanAncestorIds(props.span); 94 // Some traces have multiple root-level spans, this connects them all under one guideline and adds the 95 // necessary padding for the collapse icon on root-level spans. 96 this.ancestorIds.push('root'); 97 98 this.ancestorIds.reverse(); 99 } 100 101 /** 102 * If the mouse leaves to anywhere except another span with the same ancestor id, this span's ancestor id is 103 * removed from the set of hoverIndentGuideIds. 104 * 105 * @param {Object} event - React Synthetic event tied to mouseleave. Includes the related target which is 106 * the element the user is now hovering. 107 * @param {string} ancestorId - The span id that the user was hovering over. 108 */ 109 handleMouseLeave = (event: React.MouseEvent<HTMLSpanElement>, ancestorId: string) => { 110 if ( 111 !(event.relatedTarget instanceof HTMLSpanElement) || 112 _get(event, 'relatedTarget.dataset.ancestorId') !== ancestorId 113 ) { 114 this.props.removeHoverIndentGuideId(ancestorId); 115 } 116 }; 117 118 /** 119 * If the mouse entered this span from anywhere except another span with the same ancestor id, this span's 120 * ancestorId is added to the set of hoverIndentGuideIds. 121 * 122 * @param {Object} event - React Synthetic event tied to mouseenter. Includes the related target which is 123 * the last element the user was hovering. 124 * @param {string} ancestorId - The span id that the user is now hovering over. 125 */ 126 handleMouseEnter = (event: React.MouseEvent<HTMLSpanElement>, ancestorId: string) => { 127 if ( 128 !(event.relatedTarget instanceof HTMLSpanElement) || 129 _get(event, 'relatedTarget.dataset.ancestorId') !== ancestorId 130 ) { 131 this.props.addHoverIndentGuideId(ancestorId); 132 } 133 }; 134 135 render() { 136 const { childrenVisible, onClick, showChildrenIcon, span, theme } = this.props; 137 const { hasChildren, spanID } = span; 138 const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null; 139 const icon = showChildrenIcon && hasChildren && (childrenVisible ? <IoIosArrowDown /> : <IoChevronRight />); 140 const styles = getStyles(theme); 141 return ( 142 <span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}> 143 {this.ancestorIds.map((ancestorId) => ( 144 <span 145 key={ancestorId} 146 className={cx(styles.indentGuide, { 147 [styles.indentGuideActive]: this.props.hoverIndentGuideIds.has(ancestorId), 148 })} 149 data-ancestor-id={ancestorId} 150 data-test-id="SpanTreeOffset--indentGuide" 151 onMouseEnter={(event) => this.handleMouseEnter(event, ancestorId)} 152 onMouseLeave={(event) => this.handleMouseLeave(event, ancestorId)} 153 /> 154 ))} 155 {icon && ( 156 <span 157 className={styles.iconWrapper} 158 onMouseEnter={(event) => this.handleMouseEnter(event, spanID)} 159 onMouseLeave={(event) => this.handleMouseLeave(event, spanID)} 160 data-test-id="icon-wrapper" 161 > 162 {icon} 163 </span> 164 )} 165 </span> 166 ); 167 } 168} 169 170export default withTheme(UnthemedSpanTreeOffset); 171