pagePerformanceTables.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {useMemo} from 'react';
  2. import {Link} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as qs from 'query-string';
  5. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  6. import {
  7. COL_WIDTH_UNDEFINED,
  8. GridColumnHeader,
  9. GridColumnOrder,
  10. } from 'sentry/components/gridEditable';
  11. import {
  12. Grid,
  13. GridBody,
  14. GridBodyCell,
  15. GridRow,
  16. } from 'sentry/components/gridEditable/styles';
  17. import {t} from 'sentry/locale';
  18. import {space} from 'sentry/styles/space';
  19. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  20. import {useLocation} from 'sentry/utils/useLocation';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import {PerformanceBadge} from 'sentry/views/performance/browser/webVitals/components/performanceBadge';
  23. import {Row} from 'sentry/views/performance/browser/webVitals/utils/types';
  24. import {useTransactionWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/useTransactionWebVitalsQuery';
  25. type RowWithScore = Row & {score: number};
  26. const MAX_ROWS = 6;
  27. type Column = GridColumnHeader<keyof RowWithScore>;
  28. const columnOrder: GridColumnOrder<keyof RowWithScore>[] = [
  29. {key: 'transaction', width: COL_WIDTH_UNDEFINED, name: 'Transaction'},
  30. {key: 'count()', width: COL_WIDTH_UNDEFINED, name: 'Page Loads'},
  31. {key: 'score', width: COL_WIDTH_UNDEFINED, name: 'Score'},
  32. ];
  33. export function PagePerformanceTables() {
  34. const location = useLocation();
  35. const {projects} = useProjects();
  36. const project = useMemo(
  37. () => projects.find(p => p.id === String(location.query.project)),
  38. [projects, location.query.project]
  39. );
  40. const {data} = useTransactionWebVitalsQuery({});
  41. const tableData: RowWithScore[] = data.sort((a, b) => b['count()'] - a['count()']);
  42. const good = tableData.filter(row => row.score >= 90).slice(0, MAX_ROWS);
  43. const needsImprovement = tableData
  44. .filter(row => row.score >= 50 && row.score < 90)
  45. .slice(0, MAX_ROWS);
  46. const bad = tableData.filter(row => row.score < 50).slice(0, MAX_ROWS);
  47. function renderBodyCell(col: Column, row: RowWithScore) {
  48. const {key} = col;
  49. if (key === 'score') {
  50. return (
  51. <AlignCenter>
  52. <PerformanceBadge score={row.score} />
  53. </AlignCenter>
  54. );
  55. }
  56. if (key === 'count()') {
  57. return <AlignRight>{formatAbbreviatedNumber(row['count()'])}</AlignRight>;
  58. }
  59. if (key === 'transaction') {
  60. const link = `/performance/summary/?${qs.stringify({
  61. project: project?.id,
  62. transaction: row.transaction,
  63. })}`;
  64. return (
  65. <NoOverflow>
  66. {project && (
  67. <StyledProjectAvatar
  68. project={project}
  69. direction="left"
  70. size={16}
  71. hasTooltip
  72. tooltip={project.slug}
  73. />
  74. )}
  75. <Link to={link}>{row.transaction}</Link>
  76. </NoOverflow>
  77. );
  78. }
  79. return <NoOverflow>{row[key]}</NoOverflow>;
  80. }
  81. return (
  82. <div>
  83. <Flex>
  84. <GridContainer>
  85. <GridDescription>
  86. <GridDescriptionHeader>{t('Poor Score')}</GridDescriptionHeader>
  87. {t('Pageload scores less than 50')}
  88. </GridDescription>
  89. <StyledGrid data-test-id="grid-editable" scrollable={false}>
  90. <GridContent data={bad} renderBodyCell={renderBodyCell} />
  91. </StyledGrid>
  92. </GridContainer>
  93. <GridContainer>
  94. <GridDescription>
  95. <GridDescriptionHeader>{t('Meh Score')}</GridDescriptionHeader>
  96. {t('Pageload scores greater than or equal to 50')}
  97. </GridDescription>
  98. <StyledGrid data-test-id="grid-editable" scrollable={false}>
  99. <GridContent data={needsImprovement} renderBodyCell={renderBodyCell} />
  100. </StyledGrid>
  101. </GridContainer>
  102. <GridContainer>
  103. <GridDescription>
  104. <GridDescriptionHeader>{t('Good Score')}</GridDescriptionHeader>
  105. {t('Pageload scores greater than or equal to 90')}
  106. </GridDescription>
  107. <StyledGrid data-test-id="grid-editable" scrollable={false}>
  108. <GridContent data={good} renderBodyCell={renderBodyCell} />
  109. </StyledGrid>
  110. </GridContainer>
  111. </Flex>
  112. <ShowMore>{t('Show More')}</ShowMore>
  113. </div>
  114. );
  115. }
  116. function GridContent({data, renderBodyCell}) {
  117. return (
  118. <GridBody>
  119. {data.map(row => {
  120. return (
  121. <GridRow key={row.transaction}>
  122. {columnOrder.map(column => (
  123. <GridBodyCell key={`${row.transaction} ${column.key}`}>
  124. {renderBodyCell(column, row)}
  125. </GridBodyCell>
  126. ))}
  127. </GridRow>
  128. );
  129. })}
  130. </GridBody>
  131. );
  132. }
  133. const Flex = styled('div')`
  134. display: flex;
  135. flex-direction: row;
  136. justify-content: space-between;
  137. width: 100%;
  138. gap: ${space(2)};
  139. margin-top: ${space(2)};
  140. `;
  141. const GridContainer = styled('div')`
  142. min-width: 320px;
  143. flex: 1;
  144. `;
  145. const NoOverflow = styled('span')`
  146. overflow: hidden;
  147. text-overflow: ellipsis;
  148. white-space: nowrap;
  149. `;
  150. const AlignRight = styled('span')<{color?: string}>`
  151. text-align: right;
  152. width: 100%;
  153. ${p => (p.color ? `color: ${p.color};` : '')}
  154. `;
  155. const AlignCenter = styled('span')`
  156. text-align: center;
  157. width: 100%;
  158. `;
  159. const ShowMore = styled('div')`
  160. color: ${p => p.theme.gray300};
  161. font-size: ${p => p.theme.fontSizeMedium};
  162. text-align: center;
  163. margin-top: ${space(2)};
  164. cursor: pointer;
  165. &:hover {
  166. color: ${p => p.theme.gray400};
  167. }
  168. `;
  169. const StyledGrid = styled(Grid)`
  170. grid-template-columns: minmax(90px, auto) minmax(90px, auto) minmax(90px, auto);
  171. border: 1px solid ${p => p.theme.border};
  172. border-radius: 0 0 ${p => p.theme.borderRadius} ${p => p.theme.borderRadius};
  173. > tbody > tr:first-child td {
  174. border-top: none;
  175. }
  176. `;
  177. const GridDescription = styled('div')`
  178. font-size: ${p => p.theme.fontSizeMedium};
  179. padding: ${space(1)} ${space(2)};
  180. border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
  181. border: 1px solid ${p => p.theme.border};
  182. border-bottom: none;
  183. color: ${p => p.theme.gray300};
  184. `;
  185. const GridDescriptionHeader = styled('div')`
  186. font-weight: bold;
  187. color: ${p => p.theme.textColor};
  188. `;
  189. const StyledProjectAvatar = styled(ProjectAvatar)`
  190. top: ${space(0.25)};
  191. position: relative;
  192. padding-right: ${space(1)};
  193. `;