index.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import type {ReactNode} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Alert} from 'sentry/components/alert';
  4. import LoadingIndicator from 'sentry/components/loadingIndicator';
  5. import {PanelTable} from 'sentry/components/panels/panelTable';
  6. import {t} from 'sentry/locale';
  7. import EventView from 'sentry/utils/discover/eventView';
  8. import type {Sort} from 'sentry/utils/discover/fields';
  9. import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
  10. import {useLocation} from 'sentry/utils/useLocation';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import {useRoutes} from 'sentry/utils/useRoutes';
  13. import useUrlParams from 'sentry/utils/useUrlParams';
  14. import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
  15. import HeaderCell from 'sentry/views/replays/replayTable/headerCell';
  16. import {
  17. ActivityCell,
  18. BrowserCell,
  19. DeadClickCountCell,
  20. DurationCell,
  21. ErrorCountCell,
  22. OSCell,
  23. PlayPauseCell,
  24. RageClickCountCell,
  25. ReplayCell,
  26. TransactionCell,
  27. } from 'sentry/views/replays/replayTable/tableCell';
  28. import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
  29. import type {ReplayListRecord} from 'sentry/views/replays/types';
  30. type Props = {
  31. fetchError: null | undefined | Error;
  32. isFetching: boolean;
  33. replays: undefined | ReplayListRecord[] | ReplayListRecordWithTx[];
  34. sort: Sort | undefined;
  35. visibleColumns: ReplayColumn[];
  36. emptyMessage?: ReactNode;
  37. gridRows?: string;
  38. onClickPlay?: (index: number) => void;
  39. referrerLocation?: string;
  40. showDropdownFilters?: boolean;
  41. };
  42. function ReplayTable({
  43. fetchError,
  44. isFetching,
  45. replays,
  46. sort,
  47. visibleColumns,
  48. emptyMessage,
  49. gridRows,
  50. showDropdownFilters,
  51. onClickPlay,
  52. referrerLocation,
  53. }: Props) {
  54. const routes = useRoutes();
  55. const location = useLocation();
  56. const organization = useOrganization();
  57. // we may have a selected replay index in the URLs
  58. const urlParams = useUrlParams();
  59. const rawReplayIndex = urlParams.getParamValue('selected_replay_index');
  60. const selectedReplayIndex = parseInt(
  61. typeof rawReplayIndex === 'string' ? rawReplayIndex : '0',
  62. 10
  63. );
  64. const tableHeaders = visibleColumns
  65. .filter(Boolean)
  66. .map(column => <HeaderCell key={column} column={column} sort={sort} />);
  67. if (fetchError && !isFetching) {
  68. return (
  69. <StyledPanelTable
  70. headers={tableHeaders}
  71. isLoading={false}
  72. visibleColumns={visibleColumns}
  73. data-test-id="replay-table"
  74. gridRows={undefined}
  75. >
  76. <StyledAlert type="error" showIcon>
  77. {typeof fetchError === 'string'
  78. ? fetchError
  79. : t(
  80. 'Sorry, the list of replays could not be loaded. This could be due to invalid search parameters or an internal systems error.'
  81. )}
  82. </StyledAlert>
  83. </StyledPanelTable>
  84. );
  85. }
  86. const referrer = getRouteStringFromRoutes(routes);
  87. const eventView = EventView.fromLocation(location);
  88. return (
  89. <StyledPanelTable
  90. headers={tableHeaders}
  91. isEmpty={replays?.length === 0}
  92. isLoading={isFetching}
  93. visibleColumns={visibleColumns}
  94. disablePadding
  95. data-test-id="replay-table"
  96. emptyMessage={emptyMessage}
  97. gridRows={isFetching ? undefined : gridRows}
  98. loader={<LoadingIndicator style={{margin: '54px auto'}} />}
  99. disableHeaderBorderBottom
  100. >
  101. {replays?.map(
  102. (replay: ReplayListRecord | ReplayListRecordWithTx, index: number) => {
  103. return (
  104. <Row
  105. key={replay.id}
  106. isPlaying={index === selectedReplayIndex && referrerLocation !== 'replay'}
  107. onClick={() => onClickPlay?.(index)}
  108. showCursor={onClickPlay !== undefined}
  109. referrerLocation={referrerLocation}
  110. >
  111. {visibleColumns.map(column => {
  112. switch (column) {
  113. case ReplayColumn.ACTIVITY:
  114. return (
  115. <ActivityCell
  116. key="activity"
  117. replay={replay}
  118. showDropdownFilters={showDropdownFilters}
  119. />
  120. );
  121. case ReplayColumn.BROWSER:
  122. return (
  123. <BrowserCell
  124. key="browser"
  125. replay={replay}
  126. showDropdownFilters={showDropdownFilters}
  127. />
  128. );
  129. case ReplayColumn.COUNT_DEAD_CLICKS:
  130. return (
  131. <DeadClickCountCell
  132. key="countDeadClicks"
  133. replay={replay}
  134. showDropdownFilters={showDropdownFilters}
  135. />
  136. );
  137. case ReplayColumn.COUNT_ERRORS:
  138. return (
  139. <ErrorCountCell
  140. key="countErrors"
  141. replay={replay}
  142. showDropdownFilters={showDropdownFilters}
  143. />
  144. );
  145. case ReplayColumn.COUNT_RAGE_CLICKS:
  146. return (
  147. <RageClickCountCell
  148. key="countRageClicks"
  149. replay={replay}
  150. showDropdownFilters={showDropdownFilters}
  151. />
  152. );
  153. case ReplayColumn.DURATION:
  154. return (
  155. <DurationCell
  156. key="duration"
  157. replay={replay}
  158. showDropdownFilters={showDropdownFilters}
  159. />
  160. );
  161. case ReplayColumn.OS:
  162. return (
  163. <OSCell
  164. key="os"
  165. replay={replay}
  166. showDropdownFilters={showDropdownFilters}
  167. />
  168. );
  169. case ReplayColumn.REPLAY:
  170. return (
  171. <ReplayCell
  172. key="session"
  173. replay={replay}
  174. eventView={eventView}
  175. organization={organization}
  176. referrer={referrer}
  177. referrer_table="main"
  178. />
  179. );
  180. case ReplayColumn.PLAY_PAUSE:
  181. return (
  182. <PlayPauseCell
  183. key="play"
  184. isSelected={selectedReplayIndex === index}
  185. handleClick={() => onClickPlay?.(index)}
  186. />
  187. );
  188. case ReplayColumn.SLOWEST_TRANSACTION:
  189. return (
  190. <TransactionCell
  191. key="slowestTransaction"
  192. replay={replay}
  193. organization={organization}
  194. />
  195. );
  196. default:
  197. return null;
  198. }
  199. })}
  200. </Row>
  201. );
  202. }
  203. )}
  204. </StyledPanelTable>
  205. );
  206. }
  207. const StyledPanelTable = styled(PanelTable)<{
  208. visibleColumns: ReplayColumn[];
  209. gridRows?: string;
  210. }>`
  211. margin-bottom: 0;
  212. grid-template-columns: ${p =>
  213. p.visibleColumns
  214. .filter(Boolean)
  215. .map(column => (column === 'replay' ? 'minmax(100px, 1fr)' : 'max-content'))
  216. .join(' ')};
  217. ${props =>
  218. props.gridRows
  219. ? `grid-template-rows: ${props.gridRows};`
  220. : `grid-template-rows: 44px max-content;`}
  221. `;
  222. const StyledAlert = styled(Alert)`
  223. border-radius: 0;
  224. border-width: 1px 0 0 0;
  225. grid-column: 1/-1;
  226. margin-bottom: 0;
  227. `;
  228. const Row = styled('div')<{
  229. isPlaying?: boolean;
  230. referrerLocation?: string;
  231. showCursor?: boolean;
  232. }>`
  233. ${p =>
  234. p.referrerLocation === 'replay'
  235. ? `display: contents;
  236. & > * {
  237. border-top: 1px solid ${p.theme.border};
  238. }`
  239. : `display: contents;
  240. & > * {
  241. background-color: ${p.isPlaying ? p.theme.translucentGray200 : 'inherit'};
  242. border-top: 1px solid ${p.theme.border};
  243. cursor: ${p.showCursor ? 'pointer' : 'default'};
  244. }
  245. :hover {
  246. background-color: ${p.showCursor ? p.theme.translucentInnerBorder : 'inherit'};
  247. }
  248. :active {
  249. background-color: ${p.theme.translucentGray200};
  250. }
  251. `}
  252. `;
  253. export default ReplayTable;