index.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. 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. footer?: ReactNode;
  41. gridRows?: string;
  42. saveLocation?: boolean;
  43. };
  44. function ReplayTable({
  45. fetchError,
  46. isFetching,
  47. replays,
  48. sort,
  49. visibleColumns,
  50. emptyMessage,
  51. saveLocation,
  52. gridRows,
  53. footer,
  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.COUNT_DEAD_CLICKS) ||
  102. visibleColumns.includes(ReplayColumn.COUNT_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 <DeadClickCountCell key="countDeadClicks" replay={replay} />;
  148. case ReplayColumn.COUNT_ERRORS:
  149. return <ErrorCountCell key="countErrors" replay={replay} />;
  150. case ReplayColumn.COUNT_RAGE_CLICKS:
  151. return <RageClickCountCell key="countRageClicks" replay={replay} />;
  152. case ReplayColumn.DURATION:
  153. return <DurationCell key="duration" replay={replay} />;
  154. case ReplayColumn.OS:
  155. return <OSCell key="os" replay={replay} />;
  156. case ReplayColumn.REPLAY:
  157. return (
  158. <ReplayCell
  159. key="session"
  160. replay={replay}
  161. eventView={eventView}
  162. organization={organization}
  163. referrer={referrer}
  164. showUrl
  165. referrer_table="main"
  166. />
  167. );
  168. case ReplayColumn.SLOWEST_TRANSACTION:
  169. return (
  170. <TransactionCell
  171. key="slowestTransaction"
  172. replay={replay}
  173. organization={organization}
  174. />
  175. );
  176. case ReplayColumn.MOST_RAGE_CLICKS:
  177. return (
  178. <ReplayCell
  179. key="mostRageClicks"
  180. replay={replay}
  181. organization={organization}
  182. referrer={referrer}
  183. showUrl={false}
  184. eventView={eventView}
  185. referrer_table="rage-table"
  186. />
  187. );
  188. case ReplayColumn.MOST_DEAD_CLICKS:
  189. return (
  190. <ReplayCell
  191. key="mostDeadClicks"
  192. replay={replay}
  193. organization={organization}
  194. referrer={referrer}
  195. showUrl={false}
  196. eventView={eventView}
  197. referrer_table="dead-table"
  198. />
  199. );
  200. case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
  201. return (
  202. <ReplayCell
  203. key="mostErroneousReplays"
  204. replay={replay}
  205. organization={organization}
  206. referrer={referrer}
  207. showUrl={false}
  208. eventView={eventView}
  209. referrer_table="errors-table"
  210. />
  211. );
  212. default:
  213. return null;
  214. }
  215. })}
  216. </Fragment>
  217. );
  218. })}
  219. {footer}
  220. </StyledPanelTable>
  221. );
  222. }
  223. const flexibleColumns = [
  224. ReplayColumn.REPLAY,
  225. ReplayColumn.MOST_RAGE_CLICKS,
  226. ReplayColumn.MOST_DEAD_CLICKS,
  227. ReplayColumn.MOST_ERRONEOUS_REPLAYS,
  228. ];
  229. const StyledPanelTable = styled(PanelTable)<{
  230. visibleColumns: ReplayColumn[];
  231. gridRows?: string;
  232. }>`
  233. ${props =>
  234. props.visibleColumns.includes(ReplayColumn.MOST_RAGE_CLICKS) ||
  235. props.visibleColumns.includes(ReplayColumn.MOST_DEAD_CLICKS) ||
  236. props.visibleColumns.includes(ReplayColumn.MOST_ERRONEOUS_REPLAYS)
  237. ? `border-bottom-left-radius: 0; border-bottom-right-radius: 0;`
  238. : ``}
  239. margin-bottom: 0;
  240. grid-template-columns: ${p =>
  241. p.visibleColumns
  242. .filter(Boolean)
  243. .map(column =>
  244. flexibleColumns.includes(column) ? 'minmax(100px, 1fr)' : 'max-content'
  245. )
  246. .join(' ')};
  247. ${props =>
  248. props.gridRows
  249. ? `grid-template-rows: ${props.gridRows};`
  250. : `grid-template-rows: 44px max-content;`}
  251. `;
  252. const StyledAlert = styled(Alert)`
  253. border-radius: 0;
  254. border-width: 1px 0 0 0;
  255. grid-column: 1/-1;
  256. margin-bottom: 0;
  257. `;
  258. export default ReplayTable;