index.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. showUrl
  187. referrer_table="main"
  188. />
  189. );
  190. case ReplayColumn.SLOWEST_TRANSACTION:
  191. return (
  192. <TransactionCell
  193. key="slowestTransaction"
  194. replay={replay}
  195. organization={organization}
  196. />
  197. );
  198. case ReplayColumn.MOST_RAGE_CLICKS:
  199. return (
  200. <ReplayCell
  201. key="mostRageClicks"
  202. replay={replay}
  203. organization={organization}
  204. referrer={referrer}
  205. showUrl={false}
  206. eventView={eventView}
  207. referrer_table="rage-table"
  208. />
  209. );
  210. case ReplayColumn.MOST_DEAD_CLICKS:
  211. return (
  212. <ReplayCell
  213. key="mostDeadClicks"
  214. replay={replay}
  215. organization={organization}
  216. referrer={referrer}
  217. showUrl={false}
  218. eventView={eventView}
  219. referrer_table="dead-table"
  220. />
  221. );
  222. case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
  223. return (
  224. <ReplayCell
  225. key="mostErroneousReplays"
  226. replay={replay}
  227. organization={organization}
  228. referrer={referrer}
  229. showUrl={false}
  230. eventView={eventView}
  231. referrer_table="errors-table"
  232. />
  233. );
  234. default:
  235. return null;
  236. }
  237. })}
  238. </Fragment>
  239. );
  240. })}
  241. </StyledPanelTable>
  242. );
  243. }
  244. const flexibleColumns = [
  245. ReplayColumn.REPLAY,
  246. ReplayColumn.MOST_RAGE_CLICKS,
  247. ReplayColumn.MOST_DEAD_CLICKS,
  248. ReplayColumn.MOST_ERRONEOUS_REPLAYS,
  249. ];
  250. const StyledPanelTable = styled(PanelTable)<{
  251. visibleColumns: ReplayColumn[];
  252. gridRows?: string;
  253. }>`
  254. ${props =>
  255. props.visibleColumns.includes(ReplayColumn.MOST_RAGE_CLICKS) ||
  256. props.visibleColumns.includes(ReplayColumn.MOST_DEAD_CLICKS) ||
  257. props.visibleColumns.includes(ReplayColumn.MOST_ERRONEOUS_REPLAYS)
  258. ? `border-bottom-left-radius: 0; border-bottom-right-radius: 0;`
  259. : ``}
  260. margin-bottom: 0;
  261. grid-template-columns: ${p =>
  262. p.visibleColumns
  263. .filter(Boolean)
  264. .map(column =>
  265. flexibleColumns.includes(column) ? 'minmax(100px, 1fr)' : 'max-content'
  266. )
  267. .join(' ')};
  268. ${props =>
  269. props.gridRows
  270. ? `grid-template-rows: ${props.gridRows};`
  271. : `grid-template-rows: 44px max-content;`}
  272. `;
  273. const StyledAlert = styled(Alert)`
  274. border-radius: 0;
  275. border-width: 1px 0 0 0;
  276. grid-column: 1/-1;
  277. margin-bottom: 0;
  278. `;
  279. export default ReplayTable;