index.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import {Fragment, ReactNode} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import {Alert} from 'sentry/components/alert';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import PanelTable from 'sentry/components/panels/panelTable';
  7. import {t} from 'sentry/locale';
  8. import EventView from 'sentry/utils/discover/eventView';
  9. import type {Sort} from 'sentry/utils/discover/fields';
  10. import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
  11. import {useLocation} from 'sentry/utils/useLocation';
  12. import useOrganization from 'sentry/utils/useOrganization';
  13. import {useRoutes} from 'sentry/utils/useRoutes';
  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. RageClickCountCell,
  24. ReplayCell,
  25. TransactionCell,
  26. } from 'sentry/views/replays/replayTable/tableCell';
  27. import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
  28. import type {ReplayListRecord} from 'sentry/views/replays/types';
  29. type Props = {
  30. fetchError: undefined | Error;
  31. isFetching: boolean;
  32. replays: undefined | ReplayListRecord[] | ReplayListRecordWithTx[];
  33. sort: Sort | undefined;
  34. visibleColumns: ReplayColumn[];
  35. emptyMessage?: ReactNode;
  36. gridRows?: string;
  37. saveLocation?: boolean;
  38. showDropdownFilters?: boolean;
  39. };
  40. function ReplayTable({
  41. fetchError,
  42. isFetching,
  43. replays,
  44. sort,
  45. visibleColumns,
  46. emptyMessage,
  47. saveLocation,
  48. gridRows,
  49. showDropdownFilters,
  50. }: Props) {
  51. const routes = useRoutes();
  52. const newLocation = useLocation();
  53. const organization = useOrganization();
  54. const location: Location = saveLocation
  55. ? {
  56. pathname: '',
  57. search: '',
  58. query: {},
  59. hash: '',
  60. state: '',
  61. action: 'PUSH',
  62. key: '',
  63. }
  64. : newLocation;
  65. const tableHeaders = visibleColumns
  66. .filter(Boolean)
  67. .map(column => <HeaderCell key={column} column={column} sort={sort} />);
  68. if (fetchError && !isFetching) {
  69. return (
  70. <StyledPanelTable
  71. headers={tableHeaders}
  72. isLoading={false}
  73. visibleColumns={visibleColumns}
  74. data-test-id="replay-table"
  75. gridRows={undefined}
  76. >
  77. <StyledAlert type="error" showIcon>
  78. {typeof fetchError === 'string'
  79. ? fetchError
  80. : t(
  81. 'Sorry, the list of replays could not be loaded. This could be due to invalid search parameters or an internal systems error.'
  82. )}
  83. </StyledAlert>
  84. </StyledPanelTable>
  85. );
  86. }
  87. const referrer = getRouteStringFromRoutes(routes);
  88. const eventView = EventView.fromLocation(location);
  89. return (
  90. <StyledPanelTable
  91. headers={tableHeaders}
  92. isEmpty={replays?.length === 0}
  93. isLoading={isFetching}
  94. visibleColumns={visibleColumns}
  95. disablePadding
  96. data-test-id="replay-table"
  97. emptyMessage={emptyMessage}
  98. gridRows={isFetching ? undefined : gridRows}
  99. loader={<LoadingIndicator style={{margin: '54px auto'}} />}
  100. >
  101. {replays?.map(replay => {
  102. return (
  103. <Fragment key={replay.id}>
  104. {visibleColumns.map(column => {
  105. switch (column) {
  106. case ReplayColumn.ACTIVITY:
  107. return (
  108. <ActivityCell
  109. key="activity"
  110. replay={replay}
  111. showDropdownFilters={showDropdownFilters}
  112. />
  113. );
  114. case ReplayColumn.BROWSER:
  115. return (
  116. <BrowserCell
  117. key="browser"
  118. replay={replay}
  119. showDropdownFilters={showDropdownFilters}
  120. />
  121. );
  122. case ReplayColumn.COUNT_DEAD_CLICKS:
  123. return (
  124. <DeadClickCountCell
  125. key="countDeadClicks"
  126. replay={replay}
  127. showDropdownFilters={showDropdownFilters}
  128. />
  129. );
  130. case ReplayColumn.COUNT_DEAD_CLICKS_NO_HEADER:
  131. return (
  132. <DeadClickCountCell
  133. key="countDeadClicks"
  134. replay={replay}
  135. showDropdownFilters={false}
  136. />
  137. );
  138. case ReplayColumn.COUNT_ERRORS:
  139. return (
  140. <ErrorCountCell
  141. key="countErrors"
  142. replay={replay}
  143. showDropdownFilters={showDropdownFilters}
  144. />
  145. );
  146. case ReplayColumn.COUNT_RAGE_CLICKS:
  147. return (
  148. <RageClickCountCell
  149. key="countRageClicks"
  150. replay={replay}
  151. showDropdownFilters={showDropdownFilters}
  152. />
  153. );
  154. case ReplayColumn.COUNT_RAGE_CLICKS_NO_HEADER:
  155. return (
  156. <RageClickCountCell
  157. key="countRageClicks"
  158. replay={replay}
  159. showDropdownFilters={false}
  160. />
  161. );
  162. case ReplayColumn.DURATION:
  163. return (
  164. <DurationCell
  165. key="duration"
  166. replay={replay}
  167. showDropdownFilters={showDropdownFilters}
  168. />
  169. );
  170. case ReplayColumn.OS:
  171. return (
  172. <OSCell
  173. key="os"
  174. replay={replay}
  175. showDropdownFilters={showDropdownFilters}
  176. />
  177. );
  178. case ReplayColumn.REPLAY:
  179. return (
  180. <ReplayCell
  181. key="session"
  182. replay={replay}
  183. eventView={eventView}
  184. organization={organization}
  185. referrer={referrer}
  186. referrer_table="main"
  187. />
  188. );
  189. case ReplayColumn.SLOWEST_TRANSACTION:
  190. return (
  191. <TransactionCell
  192. key="slowestTransaction"
  193. replay={replay}
  194. organization={organization}
  195. />
  196. );
  197. case ReplayColumn.MOST_RAGE_CLICKS:
  198. return (
  199. <ReplayCell
  200. key="mostRageClicks"
  201. replay={replay}
  202. organization={organization}
  203. referrer={referrer}
  204. eventView={eventView}
  205. referrer_table="rage-table"
  206. />
  207. );
  208. case ReplayColumn.MOST_DEAD_CLICKS:
  209. return (
  210. <ReplayCell
  211. key="mostDeadClicks"
  212. replay={replay}
  213. organization={organization}
  214. referrer={referrer}
  215. eventView={eventView}
  216. referrer_table="dead-table"
  217. />
  218. );
  219. case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
  220. return (
  221. <ReplayCell
  222. key="mostErroneousReplays"
  223. replay={replay}
  224. organization={organization}
  225. referrer={referrer}
  226. eventView={eventView}
  227. referrer_table="errors-table"
  228. />
  229. );
  230. default:
  231. return null;
  232. }
  233. })}
  234. </Fragment>
  235. );
  236. })}
  237. </StyledPanelTable>
  238. );
  239. }
  240. const flexibleColumns = [
  241. ReplayColumn.REPLAY,
  242. ReplayColumn.MOST_RAGE_CLICKS,
  243. ReplayColumn.MOST_DEAD_CLICKS,
  244. ReplayColumn.MOST_ERRONEOUS_REPLAYS,
  245. ];
  246. const StyledPanelTable = styled(PanelTable)<{
  247. visibleColumns: ReplayColumn[];
  248. gridRows?: string;
  249. }>`
  250. ${props =>
  251. props.visibleColumns.includes(ReplayColumn.MOST_RAGE_CLICKS) ||
  252. props.visibleColumns.includes(ReplayColumn.MOST_DEAD_CLICKS) ||
  253. props.visibleColumns.includes(ReplayColumn.MOST_ERRONEOUS_REPLAYS)
  254. ? `border-bottom-left-radius: 0; border-bottom-right-radius: 0;`
  255. : ``}
  256. margin-bottom: 0;
  257. grid-template-columns: ${p =>
  258. p.visibleColumns
  259. .filter(Boolean)
  260. .map(column =>
  261. flexibleColumns.includes(column) ? 'minmax(100px, 1fr)' : 'max-content'
  262. )
  263. .join(' ')};
  264. ${props =>
  265. props.gridRows
  266. ? `grid-template-rows: ${props.gridRows};`
  267. : `grid-template-rows: 44px max-content;`}
  268. `;
  269. const StyledAlert = styled(Alert)`
  270. border-radius: 0;
  271. border-width: 1px 0 0 0;
  272. grid-column: 1/-1;
  273. margin-bottom: 0;
  274. `;
  275. export default ReplayTable;