eventSamplesTable.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {LinkButton} from 'sentry/components/button';
  4. import type {GridColumnHeader} from 'sentry/components/gridEditable';
  5. import GridEditable from 'sentry/components/gridEditable';
  6. import SortLink from 'sentry/components/gridEditable/sortLink';
  7. import Link from 'sentry/components/links/link';
  8. import type {CursorHandler} from 'sentry/components/pagination';
  9. import Pagination from 'sentry/components/pagination';
  10. import {Tooltip} from 'sentry/components/tooltip';
  11. import {IconProfiling} from 'sentry/icons/iconProfiling';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {defined} from 'sentry/utils';
  15. import {browserHistory} from 'sentry/utils/browserHistory';
  16. import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
  17. import type {MetaType} from 'sentry/utils/discover/eventView';
  18. import type EventView from 'sentry/utils/discover/eventView';
  19. import {isFieldSortable} from 'sentry/utils/discover/eventView';
  20. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  21. import type {Sort} from 'sentry/utils/discover/fields';
  22. import {fieldAlignment} from 'sentry/utils/discover/fields';
  23. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  24. import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
  25. import {useLocation} from 'sentry/utils/useLocation';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import type {TableColumn} from 'sentry/views/discover/table/types';
  28. import {DeviceClassSelector} from 'sentry/views/performance/mobile/screenload/screenLoadSpans/deviceClassSelector';
  29. type Props = {
  30. columnNameMap: Record<string, string>;
  31. cursorName: string;
  32. eventIdKey: 'id' | 'transaction.id';
  33. eventView: EventView;
  34. isLoading: boolean;
  35. profileIdKey: 'profile.id' | 'profile_id';
  36. sort: Sort;
  37. sortKey: string;
  38. data?: TableData;
  39. footerAlignedPagination?: boolean;
  40. pageLinks?: string;
  41. showDeviceClassSelector?: boolean;
  42. };
  43. const ICON_FIELDS = ['profile.id', 'profile_id'];
  44. export function EventSamplesTable({
  45. cursorName,
  46. sortKey,
  47. showDeviceClassSelector,
  48. eventView,
  49. data,
  50. isLoading,
  51. pageLinks,
  52. eventIdKey,
  53. profileIdKey,
  54. columnNameMap,
  55. sort,
  56. footerAlignedPagination = false,
  57. }: Props) {
  58. const location = useLocation();
  59. const organization = useOrganization();
  60. const eventViewColumns = eventView.getColumns();
  61. function renderBodyCell(column, row): React.ReactNode {
  62. if (!data?.meta || !data?.meta.fields) {
  63. return row[column.key];
  64. }
  65. if (column.key === eventIdKey) {
  66. return (
  67. <Link
  68. to={generateLinkToEventInTraceView({
  69. eventId: row[eventIdKey],
  70. projectSlug: row['project.name'],
  71. traceSlug: row.trace,
  72. timestamp: row.timestamp,
  73. organization,
  74. location,
  75. })}
  76. >
  77. {row[eventIdKey].slice(0, 8)}
  78. </Link>
  79. );
  80. }
  81. if (column.key === profileIdKey) {
  82. const profileTarget =
  83. defined(row['project.name']) && defined(row[profileIdKey])
  84. ? generateProfileFlamechartRoute({
  85. orgSlug: organization.slug,
  86. projectSlug: row['project.name'],
  87. profileId: String(row[profileIdKey]),
  88. })
  89. : null;
  90. return (
  91. <IconWrapper>
  92. {profileTarget && (
  93. <Tooltip title={t('View Profile')}>
  94. <LinkButton to={profileTarget} size="xs" aria-label={t('View Profile')}>
  95. <IconProfiling size="xs" />
  96. </LinkButton>
  97. </Tooltip>
  98. )}
  99. </IconWrapper>
  100. );
  101. }
  102. const renderer = getFieldRenderer(column.key, data?.meta.fields, false);
  103. const rendered = renderer(row, {
  104. location,
  105. organization,
  106. unit: data?.meta.units?.[column.key],
  107. });
  108. return rendered;
  109. }
  110. function renderHeadCell(
  111. column: GridColumnHeader,
  112. tableMeta?: MetaType
  113. ): React.ReactNode {
  114. const fieldType = tableMeta?.fields?.[column.key];
  115. let alignment = fieldAlignment(column.key as string, fieldType);
  116. if (ICON_FIELDS.includes(column.key as string)) {
  117. alignment = 'right';
  118. }
  119. const field = {
  120. field: column.key as string,
  121. width: column.width,
  122. };
  123. function generateSortLink() {
  124. if (!tableMeta) {
  125. return undefined;
  126. }
  127. let newSortDirection: Sort['kind'] = 'desc';
  128. if (sort?.field === column.key) {
  129. if (sort.kind === 'desc') {
  130. newSortDirection = 'asc';
  131. }
  132. }
  133. const newSort = `${newSortDirection === 'desc' ? '-' : ''}${column.key}`;
  134. return {
  135. ...location,
  136. query: {...location.query, [sortKey]: newSort},
  137. };
  138. }
  139. const canSort = isFieldSortable(field, tableMeta?.fields, true);
  140. const sortLink = (
  141. <SortLink
  142. align={alignment}
  143. title={column.name}
  144. direction={sort?.field === column.key ? sort.kind : undefined}
  145. canSort={canSort}
  146. generateSortLink={generateSortLink}
  147. />
  148. );
  149. return sortLink;
  150. }
  151. const columnSortBy = eventView.getSorts();
  152. const handleCursor: CursorHandler = (newCursor, pathname, query) => {
  153. browserHistory.push({
  154. pathname,
  155. query: {...query, [cursorName]: newCursor},
  156. });
  157. };
  158. return (
  159. <Fragment>
  160. {!footerAlignedPagination && (
  161. <Header>
  162. {showDeviceClassSelector && <DeviceClassSelector />}
  163. <StyledPagination size="xs" pageLinks={pageLinks} onCursor={handleCursor} />
  164. </Header>
  165. )}
  166. <GridContainer>
  167. <GridEditable
  168. isLoading={isLoading}
  169. data={data?.data as TableDataRow[]}
  170. columnOrder={eventViewColumns
  171. .filter((col: TableColumn<React.ReactText>) =>
  172. Object.keys(columnNameMap).includes(col.name)
  173. )
  174. .map((col: TableColumn<React.ReactText>) => {
  175. return {...col, name: columnNameMap[col.key]};
  176. })}
  177. columnSortBy={columnSortBy}
  178. location={location}
  179. grid={{
  180. renderHeadCell: column => renderHeadCell(column, data?.meta),
  181. renderBodyCell,
  182. }}
  183. />
  184. </GridContainer>
  185. <div>
  186. {footerAlignedPagination && (
  187. <StyledPagination size="xs" pageLinks={pageLinks} onCursor={handleCursor} />
  188. )}
  189. </div>
  190. </Fragment>
  191. );
  192. }
  193. const StyledPagination = styled(Pagination)`
  194. margin: 0 0 0 ${space(1)};
  195. `;
  196. const Header = styled('div')`
  197. display: grid;
  198. grid-template-columns: 1fr auto;
  199. margin-bottom: ${space(1)};
  200. align-items: center;
  201. height: 26px;
  202. `;
  203. const IconWrapper = styled('div')`
  204. text-align: right;
  205. width: 100%;
  206. height: 26px;
  207. `;
  208. // Not pretty but we need to override gridEditable styles since the original
  209. // styles have too much padding for small spaces
  210. const GridContainer = styled('div')`
  211. margin-bottom: ${space(1)};
  212. th {
  213. padding: 0 ${space(1)};
  214. }
  215. th:first-child {
  216. padding-left: ${space(2)};
  217. }
  218. th:last-child {
  219. padding-right: ${space(2)};
  220. }
  221. td {
  222. padding: ${space(0.5)} ${space(1)};
  223. }
  224. td:first-child {
  225. padding-right: ${space(1)};
  226. padding-left: ${space(2)};
  227. }
  228. `;