index.tsx 9.0 KB

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