metricReadout.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import type {ReactText} from 'react';
  2. import {Fragment} from 'react';
  3. import styled from '@emotion/styled';
  4. import Duration from 'sentry/components/duration';
  5. import FileSize from 'sentry/components/fileSize';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import {PercentChange, type Polarity} from 'sentry/components/percentChange';
  8. import {Tooltip} from 'sentry/components/tooltip';
  9. import {defined} from 'sentry/utils';
  10. import {
  11. type CountUnit,
  12. CurrencyUnit,
  13. DurationUnit,
  14. type PercentageUnit,
  15. type PercentChangeUnit,
  16. RateUnit,
  17. SizeUnit,
  18. } from 'sentry/utils/discover/fields';
  19. import {formatAbbreviatedNumber, formatRate} from 'sentry/utils/formatters';
  20. import {formatPercentage} from 'sentry/utils/number/formatPercentage';
  21. import {Block} from 'sentry/views/insights/common/views/spanSummaryPage/block';
  22. type Unit =
  23. | DurationUnit.MILLISECOND
  24. | SizeUnit.BYTE
  25. | RateUnit
  26. | CountUnit
  27. | PercentageUnit
  28. | PercentChangeUnit
  29. | CurrencyUnit;
  30. interface Props {
  31. title: string;
  32. unit: Unit;
  33. value: ReactText | undefined;
  34. align?: 'left' | 'right';
  35. isLoading?: boolean;
  36. preferredPolarity?: Polarity;
  37. tooltip?: React.ReactNode;
  38. }
  39. export function MetricReadout(props: Props) {
  40. return (
  41. <Block title={props.title} alignment={props.align}>
  42. <ReadoutContent {...props} />
  43. </Block>
  44. );
  45. }
  46. function ReadoutContent({
  47. unit,
  48. value,
  49. tooltip,
  50. align = 'right',
  51. isLoading,
  52. preferredPolarity,
  53. }: Props) {
  54. if (isLoading) {
  55. return (
  56. <LoadingContainer align={align}>
  57. <LoadingIndicator mini />
  58. </LoadingContainer>
  59. );
  60. }
  61. if (!defined(value)) {
  62. return <Fragment>--</Fragment>;
  63. }
  64. let renderedValue: React.ReactNode;
  65. if (isARateUnit(unit)) {
  66. renderedValue = (
  67. <NumberContainer align={align}>
  68. {formatRate(typeof value === 'string' ? parseFloat(value) : value, unit, {
  69. minimumValue: MINIMUM_RATE_VALUE,
  70. })}
  71. </NumberContainer>
  72. );
  73. }
  74. if (unit === DurationUnit.MILLISECOND) {
  75. // TODO: Implement other durations
  76. renderedValue = (
  77. <NumberContainer align={align}>
  78. <Duration
  79. seconds={typeof value === 'string' ? parseFloat(value) : value / 1000}
  80. fixedDigits={2}
  81. abbreviation
  82. />
  83. </NumberContainer>
  84. );
  85. }
  86. if (unit === SizeUnit.BYTE) {
  87. // TODO: Implement other sizes
  88. renderedValue = (
  89. <NumberContainer align={align}>
  90. <FileSize bytes={typeof value === 'string' ? parseInt(value, 10) : value} />
  91. </NumberContainer>
  92. );
  93. }
  94. if (unit === 'count') {
  95. renderedValue = (
  96. <NumberContainer align={align}>
  97. {formatAbbreviatedNumber(typeof value === 'string' ? parseInt(value, 10) : value)}
  98. </NumberContainer>
  99. );
  100. }
  101. if (unit === CurrencyUnit.USD) {
  102. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  103. if (numericValue <= 1) {
  104. renderedValue = (
  105. <NumberContainer align={align}>US ${numericValue.toFixed(3)}</NumberContainer>
  106. );
  107. } else {
  108. renderedValue = (
  109. <NumberContainer align={align}>
  110. US ${formatAbbreviatedNumber(numericValue)}
  111. </NumberContainer>
  112. );
  113. }
  114. }
  115. if (unit === 'percentage') {
  116. renderedValue = (
  117. <NumberContainer align={align}>
  118. {formatPercentage(
  119. typeof value === 'string' ? parseFloat(value) : value,
  120. undefined,
  121. {minimumValue: MINIMUM_PERCENTAGE_VALUE}
  122. )}
  123. </NumberContainer>
  124. );
  125. }
  126. if (unit === 'percent_change') {
  127. renderedValue = (
  128. <NumberContainer align={align}>
  129. <PercentChange
  130. value={typeof value === 'string' ? parseFloat(value) : value}
  131. minimumValue={MINIMUM_PERCENTAGE_VALUE}
  132. preferredPolarity={preferredPolarity}
  133. />
  134. </NumberContainer>
  135. );
  136. }
  137. if (tooltip) {
  138. return (
  139. <NumberContainer align={align}>
  140. <Tooltip title={tooltip} isHoverable showUnderline>
  141. {renderedValue}
  142. </Tooltip>
  143. </NumberContainer>
  144. );
  145. }
  146. return <NumberContainer align={align}>{renderedValue}</NumberContainer>;
  147. }
  148. const MINIMUM_RATE_VALUE = 0.01;
  149. const MINIMUM_PERCENTAGE_VALUE = 0.0001; // 0.01%
  150. const NumberContainer = styled('div')<{align: 'left' | 'right'}>`
  151. text-align: ${p => p.align};
  152. font-variant-numeric: tabular-nums;
  153. `;
  154. const LoadingContainer = styled('div')<{align: 'left' | 'right'}>`
  155. display: flex;
  156. justify-content: ${p => (p.align === 'right' ? 'flex-end' : 'flex-start')};
  157. align-items: center;
  158. `;
  159. function isARateUnit(unit: string): unit is RateUnit {
  160. return (Object.values(RateUnit) as string[]).includes(unit);
  161. }