webVitalsDetailPanel.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import {useMemo} from 'react';
  2. import {Link} from 'react-router';
  3. import {useTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import * as qs from 'query-string';
  6. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  7. import GridEditable, {
  8. COL_WIDTH_UNDEFINED,
  9. GridColumnHeader,
  10. GridColumnOrder,
  11. GridColumnSortBy,
  12. } from 'sentry/components/gridEditable';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {getDuration} from 'sentry/utils/formatters';
  16. import {
  17. PageErrorAlert,
  18. PageErrorProvider,
  19. } from 'sentry/utils/performance/contexts/pageError';
  20. import {useLocation} from 'sentry/utils/useLocation';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import {getScoreColor} from 'sentry/views/performance/browser/webVitals/utils/getScoreColor';
  23. import {
  24. Row,
  25. RowWithScore,
  26. WebVitals,
  27. } from 'sentry/views/performance/browser/webVitals/utils/types';
  28. import {useTransactionWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/useTransactionWebVitalsQuery';
  29. import {ClsDescription} from 'sentry/views/performance/browser/webVitals/webVitalsDescriptions/cls';
  30. import {FcpDescription} from 'sentry/views/performance/browser/webVitals/webVitalsDescriptions/fcp';
  31. import {FidDescription} from 'sentry/views/performance/browser/webVitals/webVitalsDescriptions/fid';
  32. import {LcpDescription} from 'sentry/views/performance/browser/webVitals/webVitalsDescriptions/lcp';
  33. import {TtfbDescription} from 'sentry/views/performance/browser/webVitals/webVitalsDescriptions/ttfb';
  34. import DetailPanel from 'sentry/views/starfish/components/detailPanel';
  35. type Column = GridColumnHeader;
  36. const columnOrder: GridColumnOrder[] = [
  37. {key: 'transaction', width: COL_WIDTH_UNDEFINED, name: 'Transaction'},
  38. {key: 'count()', width: COL_WIDTH_UNDEFINED, name: 'Count'},
  39. {key: 'webVital', width: COL_WIDTH_UNDEFINED, name: 'Web Vital'},
  40. {key: 'score', width: COL_WIDTH_UNDEFINED, name: 'Score'},
  41. ];
  42. const sort: GridColumnSortBy<keyof Row> = {key: 'count()', order: 'desc'};
  43. export function WebVitalsDetailPanel({
  44. webVital,
  45. onClose,
  46. }: {
  47. onClose: () => void;
  48. webVital: WebVitals | null;
  49. }) {
  50. const location = useLocation();
  51. const {projects} = useProjects();
  52. const theme = useTheme();
  53. const project = useMemo(
  54. () => projects.find(p => p.id === String(location.query.project)),
  55. [projects, location.query.project]
  56. );
  57. const {data, isLoading} = useTransactionWebVitalsQuery({
  58. orderBy: webVital,
  59. limit: 10,
  60. });
  61. const detailKey = webVital;
  62. const renderHeadCell = (col: Column) => {
  63. if (col.key === 'transaction') {
  64. return <NoOverflow>{col.name}</NoOverflow>;
  65. }
  66. if (col.key === 'webVital') {
  67. return <AlignRight>{`${webVital}`}</AlignRight>;
  68. }
  69. if (col.key === 'score') {
  70. return <AlignRight>{`${webVital} ${col.name}`}</AlignRight>;
  71. }
  72. return <AlignRight>{col.name}</AlignRight>;
  73. };
  74. const renderBodyCell = (col: Column, row: RowWithScore) => {
  75. const {key} = col;
  76. if (key === 'score') {
  77. return (
  78. <AlignRight color={getScoreColor(row[`${webVital}Score`], theme)}>
  79. {row[`${webVital}Score`]}
  80. </AlignRight>
  81. );
  82. }
  83. if (col.key === 'webVital') {
  84. let value: string | number = row[mapWebVitalToColumn(webVital)];
  85. if (webVital && ['lcp', 'fcp', 'ttfb', 'fid'].includes(webVital)) {
  86. value = getDuration(value / 1000, 2, true);
  87. } else if (webVital === 'cls') {
  88. value = value?.toFixed(2);
  89. }
  90. return <AlignRight>{value}</AlignRight>;
  91. }
  92. if (key === 'count()') {
  93. return <AlignRight>{row['count()']}</AlignRight>;
  94. }
  95. if (key === 'transaction') {
  96. const link = `/performance/summary/?${qs.stringify({
  97. project: project?.id,
  98. transaction: row.transaction,
  99. })}`;
  100. return (
  101. <NoOverflow>
  102. <Link to={link}>{row.transaction}</Link>
  103. </NoOverflow>
  104. );
  105. }
  106. return <NoOverflow>{row[key]}</NoOverflow>;
  107. };
  108. return (
  109. <PageErrorProvider>
  110. <DetailPanel detailKey={detailKey ?? undefined} onClose={onClose}>
  111. {project && (
  112. <StyledProjectAvatar
  113. project={project}
  114. direction="left"
  115. size={40}
  116. hasTooltip
  117. tooltip={project.slug}
  118. />
  119. )}
  120. {webVital === 'lcp' && <LcpDescription />}
  121. {webVital === 'fcp' && <FcpDescription />}
  122. {webVital === 'ttfb' && <TtfbDescription />}
  123. {webVital === 'cls' && <ClsDescription />}
  124. {webVital === 'fid' && <FidDescription />}
  125. <h5>{t('Pages to Improve')}</h5>
  126. <GridEditable
  127. data={data}
  128. isLoading={isLoading}
  129. columnOrder={columnOrder}
  130. columnSortBy={[sort]}
  131. grid={{
  132. renderHeadCell,
  133. renderBodyCell,
  134. }}
  135. location={location}
  136. />
  137. <PageErrorAlert />
  138. </DetailPanel>
  139. </PageErrorProvider>
  140. );
  141. }
  142. const mapWebVitalToColumn = (webVital?: WebVitals | null) => {
  143. switch (webVital) {
  144. case 'lcp':
  145. return 'p75(measurements.lcp)';
  146. case 'fcp':
  147. return 'p75(measurements.fcp)';
  148. case 'cls':
  149. return 'p75(measurements.cls)';
  150. case 'ttfb':
  151. return 'p75(measurements.ttfb)';
  152. case 'fid':
  153. return 'p75(measurements.fid)';
  154. default:
  155. return 'count()';
  156. }
  157. };
  158. const StyledProjectAvatar = styled(ProjectAvatar)`
  159. padding-top: ${space(1)};
  160. padding-bottom: ${space(2)};
  161. `;
  162. const NoOverflow = styled('span')`
  163. overflow: hidden;
  164. text-overflow: ellipsis;
  165. `;
  166. const AlignRight = styled('span')<{color?: string}>`
  167. text-align: right;
  168. width: 100%;
  169. ${p => (p.color ? `color: ${p.color};` : '')}
  170. `;