webVitalMeters.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import styled from '@emotion/styled';
  2. import {t} from 'sentry/locale';
  3. import {space} from 'sentry/styles/space';
  4. import {formatAbbreviatedNumber, getDuration} from 'sentry/utils/formatters';
  5. import {ProjectScore} from 'sentry/views/performance/browser/webVitals/utils/calculatePerformanceScore';
  6. import {PERFORMANCE_SCORE_COLORS} from 'sentry/views/performance/browser/webVitals/utils/performanceScoreColors';
  7. import {
  8. scoreToStatus,
  9. STATUS_TEXT,
  10. } from 'sentry/views/performance/browser/webVitals/utils/scoreToStatus';
  11. import {WebVitals} from 'sentry/views/performance/browser/webVitals/utils/types';
  12. type Props = {
  13. projectData: any;
  14. // TODO: type
  15. projectScore: ProjectScore;
  16. onClick?: (webVital: WebVitals) => void;
  17. };
  18. export default function WebVitalMeters({onClick, projectData, projectScore}: Props) {
  19. const betterGetDuration = (value: number) => {
  20. return getDuration(value, value < 1000 ? 0 : 2, true);
  21. };
  22. return (
  23. <Container>
  24. <Flex>
  25. <MeterBarContainer key="lcp" onClick={() => onClick?.('lcp')}>
  26. <MeterBarBody>
  27. <MeterHeader>{t('Largest Contentful Paint (P75)')}</MeterHeader>
  28. <MeterValueText>
  29. {betterGetDuration(
  30. (projectData?.data?.[0]?.['p75(measurements.lcp)'] as number) / 1000
  31. )}
  32. </MeterValueText>
  33. </MeterBarBody>
  34. <MeterBarFooter score={projectScore.lcpScore} />
  35. </MeterBarContainer>
  36. <MeterBarContainer key="fcp" onClick={() => onClick?.('fcp')}>
  37. <MeterBarBody>
  38. <MeterHeader>{t('First Contentful Paint (P75)')}</MeterHeader>
  39. <MeterValueText>
  40. {betterGetDuration(
  41. (projectData?.data?.[0]?.['p75(measurements.fcp)'] as number) / 1000
  42. )}
  43. </MeterValueText>
  44. </MeterBarBody>
  45. <MeterBarFooter score={projectScore.fcpScore} />
  46. </MeterBarContainer>
  47. <MeterBarContainer key="fid" onClick={() => onClick?.('fid')}>
  48. <MeterBarBody>
  49. <MeterHeader>{t('First Input Delay (P75)')}</MeterHeader>
  50. <MeterValueText>
  51. {betterGetDuration(
  52. (projectData?.data?.[0]?.['p75(measurements.fid)'] as number) / 1000
  53. )}
  54. </MeterValueText>
  55. </MeterBarBody>
  56. <MeterBarFooter score={projectScore.fidScore} />
  57. </MeterBarContainer>
  58. <MeterBarContainer key="cls" onClick={() => onClick?.('cls')}>
  59. <MeterBarBody>
  60. <MeterHeader>{t('Cumulative Layout Shift (P75)')}</MeterHeader>
  61. <MeterValueText>
  62. {formatAbbreviatedNumber(
  63. projectData?.data?.[0]?.['p75(measurements.cls)'] as number,
  64. 2
  65. )}
  66. </MeterValueText>
  67. </MeterBarBody>
  68. <MeterBarFooter score={projectScore.clsScore} />
  69. </MeterBarContainer>
  70. <MeterBarContainer key="ttfb" onClick={() => onClick?.('ttfb')}>
  71. <MeterBarBody>
  72. <MeterHeader>{t('Time To First Byte (P75)')}</MeterHeader>
  73. <MeterValueText>
  74. {betterGetDuration(
  75. (projectData?.data?.[0]?.['p75(measurements.ttfb)'] as number) / 1000
  76. )}
  77. </MeterValueText>
  78. </MeterBarBody>
  79. <MeterBarFooter score={projectScore.ttfbScore} />
  80. </MeterBarContainer>
  81. </Flex>
  82. </Container>
  83. );
  84. }
  85. const Container = styled('div')`
  86. margin-top: ${space(2)};
  87. `;
  88. const Flex = styled('div')<{gap?: number}>`
  89. display: flex;
  90. flex-direction: row;
  91. justify-content: space-between;
  92. width: 100%;
  93. gap: ${p => (p.gap ? `${p.gap}px` : space(2))};
  94. align-items: center;
  95. `;
  96. const MeterBarContainer = styled('div')`
  97. flex: 1;
  98. top: -6px;
  99. position: relative;
  100. padding: 0;
  101. cursor: pointer;
  102. min-width: 200px;
  103. `;
  104. const MeterBarBody = styled('div')`
  105. border: 1px solid ${p => p.theme.gray200};
  106. border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
  107. border-bottom: none;
  108. padding: ${space(1)} 0 ${space(0.5)} 0;
  109. `;
  110. const MeterHeader = styled('div')`
  111. font-size: 13px;
  112. color: ${p => p.theme.textColor};
  113. font-weight: bold;
  114. display: inline-block;
  115. white-space: nowrap;
  116. text-align: center;
  117. width: 100%;
  118. `;
  119. const MeterValueText = styled('div')`
  120. font-size: ${p => p.theme.headerFontSize};
  121. color: ${p => p.theme.textColor};
  122. flex: 1;
  123. text-align: center;
  124. `;
  125. function MeterBarFooter({score}: {score: number}) {
  126. const status = scoreToStatus(score);
  127. return (
  128. <MeterBarFooterContainer status={status}>
  129. {STATUS_TEXT[status]} {score}
  130. </MeterBarFooterContainer>
  131. );
  132. }
  133. const MeterBarFooterContainer = styled('div')<{status: string}>`
  134. color: ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].normal]};
  135. border-radius: 0 0 ${p => p.theme.borderRadius} ${p => p.theme.borderRadius};
  136. background-color: ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].light]};
  137. border: solid 1px ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].normal]};
  138. font-size: ${p => p.theme.fontSizeExtraSmall};
  139. padding: ${space(0.5)};
  140. text-align: center;
  141. `;