eventSamplesTable.tsx 7.3 KB

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