index.tsx 11 KB

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