index.tsx 9.3 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 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. DeadClickCountWithDropdownCell,
  24. DurationCell,
  25. ErrorCountCell,
  26. OSCell,
  27. RageClickCountCell,
  28. RageClickCountWithDropdownCell,
  29. ReplayCell,
  30. TransactionCell,
  31. } from 'sentry/views/replays/replayTable/tableCell';
  32. import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
  33. import type {ReplayListRecord} from 'sentry/views/replays/types';
  34. export const MIN_DEAD_RAGE_CLICK_SDK = '7.60.1';
  35. type Props = {
  36. fetchError: undefined | Error;
  37. isFetching: boolean;
  38. replays: undefined | ReplayListRecord[] | ReplayListRecordWithTx[];
  39. sort: Sort | undefined;
  40. visibleColumns: ReplayColumn[];
  41. emptyMessage?: ReactNode;
  42. gridRows?: string;
  43. saveLocation?: boolean;
  44. };
  45. function ReplayTable({
  46. fetchError,
  47. isFetching,
  48. replays,
  49. sort,
  50. visibleColumns,
  51. emptyMessage,
  52. saveLocation,
  53. gridRows,
  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 <ActivityCell key="activity" replay={replay} />;
  144. case ReplayColumn.BROWSER:
  145. return <BrowserCell key="browser" replay={replay} />;
  146. case ReplayColumn.COUNT_DEAD_CLICKS:
  147. return (
  148. <DeadClickCountWithDropdownCell
  149. key="countDeadClicks"
  150. replay={replay}
  151. />
  152. );
  153. case ReplayColumn.COUNT_DEAD_CLICKS_NO_HEADER:
  154. return <DeadClickCountCell key="countDeadClicks" replay={replay} />;
  155. case ReplayColumn.COUNT_ERRORS:
  156. return <ErrorCountCell key="countErrors" replay={replay} />;
  157. case ReplayColumn.COUNT_RAGE_CLICKS:
  158. return (
  159. <RageClickCountWithDropdownCell
  160. key="countRageClicks"
  161. replay={replay}
  162. />
  163. );
  164. case ReplayColumn.COUNT_RAGE_CLICKS_NO_HEADER:
  165. return <RageClickCountCell key="countRageClicks" replay={replay} />;
  166. case ReplayColumn.DURATION:
  167. return <DurationCell key="duration" replay={replay} />;
  168. case ReplayColumn.OS:
  169. return <OSCell key="os" replay={replay} />;
  170. case ReplayColumn.REPLAY:
  171. return (
  172. <ReplayCell
  173. key="session"
  174. replay={replay}
  175. eventView={eventView}
  176. organization={organization}
  177. referrer={referrer}
  178. showUrl
  179. referrer_table="main"
  180. />
  181. );
  182. case ReplayColumn.SLOWEST_TRANSACTION:
  183. return (
  184. <TransactionCell
  185. key="slowestTransaction"
  186. replay={replay}
  187. organization={organization}
  188. />
  189. );
  190. case ReplayColumn.MOST_RAGE_CLICKS:
  191. return (
  192. <ReplayCell
  193. key="mostRageClicks"
  194. replay={replay}
  195. organization={organization}
  196. referrer={referrer}
  197. showUrl={false}
  198. eventView={eventView}
  199. referrer_table="rage-table"
  200. />
  201. );
  202. case ReplayColumn.MOST_DEAD_CLICKS:
  203. return (
  204. <ReplayCell
  205. key="mostDeadClicks"
  206. replay={replay}
  207. organization={organization}
  208. referrer={referrer}
  209. showUrl={false}
  210. eventView={eventView}
  211. referrer_table="dead-table"
  212. />
  213. );
  214. case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
  215. return (
  216. <ReplayCell
  217. key="mostErroneousReplays"
  218. replay={replay}
  219. organization={organization}
  220. referrer={referrer}
  221. showUrl={false}
  222. eventView={eventView}
  223. referrer_table="errors-table"
  224. />
  225. );
  226. default:
  227. return null;
  228. }
  229. })}
  230. </Fragment>
  231. );
  232. })}
  233. </StyledPanelTable>
  234. );
  235. }
  236. const flexibleColumns = [
  237. ReplayColumn.REPLAY,
  238. ReplayColumn.MOST_RAGE_CLICKS,
  239. ReplayColumn.MOST_DEAD_CLICKS,
  240. ReplayColumn.MOST_ERRONEOUS_REPLAYS,
  241. ];
  242. const StyledPanelTable = styled(PanelTable)<{
  243. visibleColumns: ReplayColumn[];
  244. gridRows?: string;
  245. }>`
  246. ${props =>
  247. props.visibleColumns.includes(ReplayColumn.MOST_RAGE_CLICKS) ||
  248. props.visibleColumns.includes(ReplayColumn.MOST_DEAD_CLICKS) ||
  249. props.visibleColumns.includes(ReplayColumn.MOST_ERRONEOUS_REPLAYS)
  250. ? `border-bottom-left-radius: 0; border-bottom-right-radius: 0;`
  251. : ``}
  252. margin-bottom: 0;
  253. grid-template-columns: ${p =>
  254. p.visibleColumns
  255. .filter(Boolean)
  256. .map(column =>
  257. flexibleColumns.includes(column) ? 'minmax(100px, 1fr)' : 'max-content'
  258. )
  259. .join(' ')};
  260. ${props =>
  261. props.gridRows
  262. ? `grid-template-rows: ${props.gridRows};`
  263. : `grid-template-rows: 44px max-content;`}
  264. `;
  265. const StyledAlert = styled(Alert)`
  266. border-radius: 0;
  267. border-width: 1px 0 0 0;
  268. grid-column: 1/-1;
  269. margin-bottom: 0;
  270. `;
  271. export default ReplayTable;