measurementsPanel.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import {Component, createRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import {toPercent} from 'sentry/components/performance/waterfall/utils';
  4. import Tooltip from 'sentry/components/tooltip';
  5. import space from 'sentry/styles/space';
  6. import {EventTransaction} from 'sentry/types/event';
  7. import {defined} from 'sentry/utils';
  8. import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
  9. import {Vital} from 'sentry/utils/performance/vitals/types';
  10. import {
  11. getMeasurementBounds,
  12. getMeasurements,
  13. SpanBoundsType,
  14. SpanGeneratedBoundsType,
  15. } from './utils';
  16. type Props = {
  17. dividerPosition: number;
  18. event: EventTransaction;
  19. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
  20. };
  21. function MeasurementsPanel(props: Props) {
  22. const {event, generateBounds, dividerPosition} = props;
  23. const measurements = getMeasurements(event, generateBounds);
  24. return (
  25. <Container
  26. style={{
  27. // the width of this component is shrunk to compensate for half of the width of the divider line
  28. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  29. }}
  30. >
  31. {Array.from(measurements.values()).map(verticalMark => {
  32. const mark = Object.values(verticalMark.marks)[0];
  33. const {timestamp} = mark;
  34. const bounds = getMeasurementBounds(timestamp, generateBounds);
  35. const shouldDisplay = defined(bounds.left) && defined(bounds.width);
  36. if (!shouldDisplay || !bounds.isSpanVisibleInView) {
  37. return null;
  38. }
  39. // Measurements are referred to by their full name `measurements.<name>`
  40. // here but are stored using their abbreviated name `<name>`. Make sure
  41. // to convert it appropriately.
  42. const vitals: Vital[] = Object.keys(verticalMark.marks).map(
  43. name => WEB_VITAL_DETAILS[`measurements.${name}`]
  44. );
  45. if (vitals.length > 1) {
  46. return (
  47. <MultiLabelContainer
  48. key={String(timestamp)}
  49. failedThreshold={verticalMark.failedThreshold}
  50. left={toPercent(bounds.left || 0)}
  51. vitals={vitals}
  52. />
  53. );
  54. }
  55. return (
  56. <LabelContainer
  57. key={String(timestamp)}
  58. failedThreshold={verticalMark.failedThreshold}
  59. left={toPercent(bounds.left || 0)}
  60. vital={vitals[0]}
  61. />
  62. );
  63. })}
  64. </Container>
  65. );
  66. }
  67. const Container = styled('div')`
  68. position: relative;
  69. overflow: hidden;
  70. height: 20px;
  71. `;
  72. const StyledMultiLabelContainer = styled('div')`
  73. transform: translateX(-50%);
  74. position: absolute;
  75. display: flex;
  76. top: 0;
  77. height: 100%;
  78. user-select: none;
  79. white-space: nowrap;
  80. `;
  81. const StyledLabelContainer = styled('div')`
  82. position: absolute;
  83. top: 0;
  84. height: 100%;
  85. user-select: none;
  86. white-space: nowrap;
  87. `;
  88. const Label = styled('div')<{
  89. failedThreshold: boolean;
  90. isSingleLabel?: boolean;
  91. }>`
  92. transform: ${p => (p.isSingleLabel ? `translate(-50%, 15%)` : `translateY(15%)`)};
  93. font-size: ${p => p.theme.fontSizeExtraSmall};
  94. font-weight: 600;
  95. color: ${p => (p.failedThreshold ? `${p.theme.red300}` : `${p.theme.gray500}`)};
  96. background: ${p => p.theme.white};
  97. border: 1px solid;
  98. border-color: ${p => (p.failedThreshold ? p.theme.red300 : p.theme.gray100)};
  99. border-radius: ${p => p.theme.borderRadius};
  100. height: 75%;
  101. display: flex;
  102. justify-content: center;
  103. align-items: center;
  104. padding: ${space(0.25)};
  105. margin-right: ${space(0.25)};
  106. `;
  107. export default MeasurementsPanel;
  108. type LabelContainerProps = {
  109. failedThreshold: boolean;
  110. left: string;
  111. vital: Vital;
  112. };
  113. type LabelContainerState = {
  114. width: number;
  115. };
  116. class LabelContainer extends Component<LabelContainerProps> {
  117. state: LabelContainerState = {
  118. width: 1,
  119. };
  120. componentDidMount() {
  121. const {current} = this.elementDOMRef;
  122. if (current) {
  123. // eslint-disable-next-line react/no-did-mount-set-state
  124. this.setState({
  125. width: current.clientWidth,
  126. });
  127. }
  128. }
  129. elementDOMRef = createRef<HTMLDivElement>();
  130. render() {
  131. const {left, failedThreshold, vital} = this.props;
  132. return (
  133. <StyledLabelContainer
  134. ref={this.elementDOMRef}
  135. style={{
  136. left: `clamp(calc(0.5 * ${this.state.width}px), ${left}, calc(100% - 0.5 * ${this.state.width}px))`,
  137. }}
  138. >
  139. <Label failedThreshold={failedThreshold} isSingleLabel>
  140. <Tooltip title={vital.name} position="top" containerDisplayMode="inline-block">
  141. {vital.acronym}
  142. </Tooltip>
  143. </Label>
  144. </StyledLabelContainer>
  145. );
  146. }
  147. }
  148. type MultiLabelContainerProps = Omit<LabelContainerProps, 'vital'> & {
  149. vitals: Vital[];
  150. };
  151. class MultiLabelContainer extends Component<MultiLabelContainerProps> {
  152. state: LabelContainerState = {
  153. width: 1,
  154. };
  155. componentDidMount() {
  156. const {current} = this.elementDOMRef;
  157. if (current) {
  158. // eslint-disable-next-line react/no-did-mount-set-state
  159. this.setState({
  160. width: current.clientWidth,
  161. });
  162. }
  163. }
  164. elementDOMRef = createRef<HTMLDivElement>();
  165. render() {
  166. const {left, failedThreshold, vitals} = this.props;
  167. return (
  168. <StyledMultiLabelContainer
  169. ref={this.elementDOMRef}
  170. style={{
  171. left: `clamp(calc(0.5 * ${this.state.width}px), ${left}, calc(100% - 0.5 * ${this.state.width}px))`,
  172. }}
  173. >
  174. {vitals.map(vital => (
  175. <Label failedThreshold={failedThreshold} key={`${vital.name}-label`}>
  176. <Tooltip
  177. title={vital.name}
  178. position="top"
  179. containerDisplayMode="inline-block"
  180. >
  181. {vital.acronym}
  182. </Tooltip>
  183. </Label>
  184. ))}
  185. </StyledMultiLabelContainer>
  186. );
  187. }
  188. }