metricReadout.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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 {space} from 'sentry/styles/space';
  10. import {defined} from 'sentry/utils';
  11. import {
  12. type CountUnit,
  13. CurrencyUnit,
  14. DurationUnit,
  15. type PercentageUnit,
  16. type PercentChangeUnit,
  17. RateUnit,
  18. SizeUnit,
  19. } from 'sentry/utils/discover/fields';
  20. import {formatAbbreviatedNumber, formatRate} from 'sentry/utils/formatters';
  21. import {formatPercentage} from 'sentry/utils/number/formatPercentage';
  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. isLoading?: boolean;
  35. preferredPolarity?: Polarity;
  36. tooltip?: React.ReactNode;
  37. }
  38. export function MetricReadout(props: Props) {
  39. return (
  40. <ReadoutWrapper>
  41. <ReadoutTitle alignment={'left'}>{props.title}</ReadoutTitle>
  42. <ReadoutContentWrapper alignment={'left'}>
  43. <ReadoutContent {...props} />
  44. </ReadoutContentWrapper>
  45. </ReadoutWrapper>
  46. );
  47. }
  48. function ReadoutContent({unit, value, tooltip, isLoading, preferredPolarity}: Props) {
  49. if (isLoading) {
  50. return (
  51. <LoadingContainer align="left">
  52. <LoadingIndicator mini />
  53. </LoadingContainer>
  54. );
  55. }
  56. if (!defined(value)) {
  57. return <Fragment>--</Fragment>;
  58. }
  59. let renderedValue: React.ReactNode;
  60. if (isARateUnit(unit)) {
  61. renderedValue = (
  62. <NumberContainer align="left">
  63. {formatRate(typeof value === 'string' ? parseFloat(value) : value, unit, {
  64. minimumValue: MINIMUM_RATE_VALUE,
  65. })}
  66. </NumberContainer>
  67. );
  68. }
  69. if (unit === DurationUnit.MILLISECOND) {
  70. // TODO: Implement other durations
  71. renderedValue = (
  72. <NumberContainer align="left">
  73. <Duration
  74. seconds={typeof value === 'string' ? parseFloat(value) : value / 1000}
  75. fixedDigits={2}
  76. abbreviation
  77. />
  78. </NumberContainer>
  79. );
  80. }
  81. if (unit === SizeUnit.BYTE) {
  82. // TODO: Implement other sizes
  83. renderedValue = (
  84. <NumberContainer align="left">
  85. <FileSize bytes={typeof value === 'string' ? parseInt(value, 10) : value} />
  86. </NumberContainer>
  87. );
  88. }
  89. if (unit === 'count') {
  90. renderedValue = (
  91. <NumberContainer align="left">
  92. {formatAbbreviatedNumber(typeof value === 'string' ? parseInt(value, 10) : value)}
  93. </NumberContainer>
  94. );
  95. }
  96. if (unit === CurrencyUnit.USD) {
  97. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  98. if (numericValue <= 1) {
  99. renderedValue = (
  100. <NumberContainer align="left">US ${numericValue.toFixed(3)}</NumberContainer>
  101. );
  102. } else {
  103. renderedValue = (
  104. <NumberContainer align="left">
  105. US ${formatAbbreviatedNumber(numericValue)}
  106. </NumberContainer>
  107. );
  108. }
  109. }
  110. if (unit === 'percentage') {
  111. renderedValue = (
  112. <NumberContainer align="left">
  113. {formatPercentage(
  114. typeof value === 'string' ? parseFloat(value) : value,
  115. undefined,
  116. {minimumValue: MINIMUM_PERCENTAGE_VALUE}
  117. )}
  118. </NumberContainer>
  119. );
  120. }
  121. if (unit === 'percent_change') {
  122. renderedValue = (
  123. <NumberContainer align="left">
  124. <PercentChange
  125. value={typeof value === 'string' ? parseFloat(value) : value}
  126. minimumValue={MINIMUM_PERCENTAGE_VALUE}
  127. preferredPolarity={preferredPolarity}
  128. />
  129. </NumberContainer>
  130. );
  131. }
  132. if (tooltip) {
  133. return (
  134. <NumberContainer align="left">
  135. <Tooltip title={tooltip} isHoverable showUnderline>
  136. {renderedValue}
  137. </Tooltip>
  138. </NumberContainer>
  139. );
  140. }
  141. return <NumberContainer align="left">{renderedValue}</NumberContainer>;
  142. }
  143. const MINIMUM_RATE_VALUE = 0.01;
  144. const MINIMUM_PERCENTAGE_VALUE = 0.0001; // 0.01%
  145. const NumberContainer = styled('div')<{align: 'left' | 'right'}>`
  146. text-align: ${p => p.align};
  147. font-variant-numeric: tabular-nums;
  148. `;
  149. const LoadingContainer = styled('div')<{align: 'left' | 'right'}>`
  150. display: flex;
  151. justify-content: ${p => (p.align === 'right' ? 'flex-end' : 'flex-start')};
  152. align-items: center;
  153. `;
  154. function isARateUnit(unit: string): unit is RateUnit {
  155. return (Object.values(RateUnit) as string[]).includes(unit);
  156. }
  157. const ReadoutWrapper = styled('div')`
  158. flex-grow: 0;
  159. min-width: 0;
  160. word-break: break-word;
  161. `;
  162. const ReadoutTitle = styled('h3')<{alignment: 'left' | 'right'}>`
  163. color: ${p => p.theme.gray300};
  164. font-size: ${p => p.theme.fontSizeMedium};
  165. margin: 0;
  166. white-space: nowrap;
  167. height: ${space(3)};
  168. text-align: ${p => p.alignment};
  169. `;
  170. const ReadoutContentWrapper = styled('h4')<{alignment: 'left' | 'right'}>`
  171. margin: 0;
  172. font-weight: ${p => p.theme.fontWeightNormal};
  173. text-align: ${p => p.alignment};
  174. `;