metricReadout.tsx 4.0 KB

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