eventSamplesTable.tsx 7.8 KB

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