eventSamplesTable.tsx 7.2 KB

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