eventSamplesTable.tsx 7.6 KB

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