profileTransactionsTable.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import {useMemo} from 'react';
  2. import Count from 'sentry/components/count';
  3. import DateTime from 'sentry/components/dateTime';
  4. import GridEditable, {
  5. COL_WIDTH_UNDEFINED,
  6. GridColumnOrder,
  7. } from 'sentry/components/gridEditable';
  8. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  9. import Link from 'sentry/components/links/link';
  10. import PerformanceDuration from 'sentry/components/performanceDuration';
  11. import {t} from 'sentry/locale';
  12. import {ProfileTransaction} from 'sentry/types/profiling/core';
  13. import {defined} from 'sentry/utils';
  14. import {Container, NumberContainer} from 'sentry/utils/discover/styles';
  15. import {generateProfileSummaryRouteWithQuery} from 'sentry/utils/profiling/routes';
  16. import {renderTableHead} from 'sentry/utils/profiling/tableRenderer';
  17. import {decodeScalar} from 'sentry/utils/queryString';
  18. import {useLocation} from 'sentry/utils/useLocation';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import useProjects from 'sentry/utils/useProjects';
  21. interface ProfileTransactionsTableProps {
  22. error: string | null;
  23. isLoading: boolean;
  24. sort: string;
  25. transactions: ProfileTransaction[];
  26. }
  27. function ProfileTransactionsTable(props: ProfileTransactionsTableProps) {
  28. const location = useLocation();
  29. const organization = useOrganization();
  30. const {projects} = useProjects();
  31. const sort = useMemo(() => {
  32. let column = decodeScalar(props.sort, '-count()');
  33. let order: 'asc' | 'desc' = 'asc' as const;
  34. if (column.startsWith('-')) {
  35. column = column.substring(1);
  36. order = 'desc' as const;
  37. }
  38. if (!SORTABLE_COLUMNS.has(column as any)) {
  39. column = 'count()';
  40. }
  41. return {
  42. key: column as TableColumnKey,
  43. order,
  44. };
  45. }, [props.sort]);
  46. const transactions: TableDataRow[] = useMemo(() => {
  47. return props.transactions.map(transaction => {
  48. const project = projects.find(proj => proj.id === transaction.project_id);
  49. return {
  50. _transactionName: transaction.name,
  51. transaction: project ? (
  52. <Link
  53. to={generateProfileSummaryRouteWithQuery({
  54. query: location.query,
  55. orgSlug: organization.slug,
  56. projectSlug: project.slug,
  57. transaction: transaction.name,
  58. })}
  59. >
  60. {transaction.name}
  61. </Link>
  62. ) : (
  63. transaction.name
  64. ),
  65. 'count()': transaction.profiles_count,
  66. project,
  67. 'p50()': transaction.duration_ms.p50,
  68. 'p75()': transaction.duration_ms.p75,
  69. 'p90()': transaction.duration_ms.p90,
  70. 'p95()': transaction.duration_ms.p95,
  71. 'p99()': transaction.duration_ms.p99,
  72. 'last_seen()': transaction.last_profile_at,
  73. };
  74. });
  75. }, [props.transactions, location, organization, projects]);
  76. const generateSortLink = (column: string) => () => {
  77. let dir = 'desc';
  78. if (column === sort.key && sort.order === dir) {
  79. dir = 'asc';
  80. }
  81. return {
  82. ...location,
  83. query: {
  84. ...location.query,
  85. sort: `${dir === 'desc' ? '-' : ''}${column}`,
  86. },
  87. };
  88. };
  89. return (
  90. <GridEditable
  91. isLoading={props.isLoading}
  92. error={props.error}
  93. data={transactions}
  94. columnOrder={COLUMN_ORDER.map(key => COLUMNS[key])}
  95. columnSortBy={[sort]}
  96. grid={{
  97. renderHeadCell: renderTableHead<string>({
  98. generateSortLink,
  99. sortableColumns: SORTABLE_COLUMNS,
  100. currentSort: sort,
  101. rightAlignedColumns: RIGHT_ALIGNED_COLUMNS,
  102. }),
  103. renderBodyCell: renderTableBody,
  104. }}
  105. location={location}
  106. />
  107. );
  108. }
  109. const RIGHT_ALIGNED_COLUMNS = new Set<TableColumnKey>([
  110. 'count()',
  111. 'p50()',
  112. 'p75()',
  113. 'p90()',
  114. 'p95()',
  115. 'p99()',
  116. ]);
  117. const SORTABLE_COLUMNS = new Set<TableColumnKey>([
  118. 'project',
  119. 'transaction',
  120. 'count()',
  121. 'p50()',
  122. 'p75()',
  123. 'p90()',
  124. 'p95()',
  125. 'p99()',
  126. 'last_seen()',
  127. ]);
  128. function renderTableBody(
  129. column: GridColumnOrder,
  130. dataRow: TableDataRow,
  131. rowIndex: number,
  132. columnIndex: number
  133. ) {
  134. return (
  135. <ProfilingTransactionsTableCell
  136. column={column}
  137. dataRow={dataRow}
  138. rowIndex={rowIndex}
  139. columnIndex={columnIndex}
  140. />
  141. );
  142. }
  143. interface ProfilingTransactionsTableCellProps {
  144. column: GridColumnOrder;
  145. columnIndex: number;
  146. dataRow: TableDataRow;
  147. rowIndex: number;
  148. }
  149. function ProfilingTransactionsTableCell({
  150. column,
  151. dataRow,
  152. }: ProfilingTransactionsTableCellProps) {
  153. const value = dataRow[column.key];
  154. switch (column.key) {
  155. case 'project':
  156. if (!defined(value)) {
  157. // should never happen but just in case
  158. return <Container>{t('n/a')}</Container>;
  159. }
  160. return (
  161. <Container>
  162. <ProjectBadge project={value} avatarSize={16} />
  163. </Container>
  164. );
  165. case 'count()':
  166. return (
  167. <NumberContainer>
  168. <Count value={value} />
  169. </NumberContainer>
  170. );
  171. case 'p50()':
  172. case 'p75()':
  173. case 'p90()':
  174. case 'p95()':
  175. case 'p99()':
  176. return (
  177. <NumberContainer>
  178. <PerformanceDuration milliseconds={value} abbreviation />
  179. </NumberContainer>
  180. );
  181. case 'last_seen()':
  182. return (
  183. <Container>
  184. <DateTime date={value} year seconds timeZone />
  185. </Container>
  186. );
  187. default:
  188. return <Container>{value}</Container>;
  189. }
  190. }
  191. type TableColumnKey =
  192. | 'transaction'
  193. | 'count()'
  194. | 'project'
  195. | 'p50()'
  196. | 'p75()'
  197. | 'p90()'
  198. | 'p95()'
  199. | 'p99()'
  200. | 'last_seen()';
  201. type TableDataRow = Record<TableColumnKey, any>;
  202. type TableColumn = GridColumnOrder<TableColumnKey>;
  203. const COLUMN_ORDER: TableColumnKey[] = [
  204. 'transaction',
  205. 'project',
  206. 'last_seen()',
  207. 'p75()',
  208. 'p95()',
  209. 'count()',
  210. ];
  211. const COLUMNS: Record<TableColumnKey, TableColumn> = {
  212. transaction: {
  213. key: 'transaction',
  214. name: t('Transaction'),
  215. width: COL_WIDTH_UNDEFINED,
  216. },
  217. 'count()': {
  218. key: 'count()',
  219. name: t('Count'),
  220. width: COL_WIDTH_UNDEFINED,
  221. },
  222. project: {
  223. key: 'project',
  224. name: t('Project'),
  225. width: COL_WIDTH_UNDEFINED,
  226. },
  227. 'p50()': {
  228. key: 'p50()',
  229. name: t('P50'),
  230. width: COL_WIDTH_UNDEFINED,
  231. },
  232. 'p75()': {
  233. key: 'p75()',
  234. name: t('P75'),
  235. width: COL_WIDTH_UNDEFINED,
  236. },
  237. 'p90()': {
  238. key: 'p90()',
  239. name: t('P90'),
  240. width: COL_WIDTH_UNDEFINED,
  241. },
  242. 'p95()': {
  243. key: 'p95()',
  244. name: t('P95'),
  245. width: COL_WIDTH_UNDEFINED,
  246. },
  247. 'p99()': {
  248. key: 'p99()',
  249. name: t('P99'),
  250. width: COL_WIDTH_UNDEFINED,
  251. },
  252. 'last_seen()': {
  253. key: 'last_seen()',
  254. name: t('Last Seen'),
  255. width: COL_WIDTH_UNDEFINED,
  256. },
  257. };
  258. export {ProfileTransactionsTable};