index.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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. };
  43. function ReplayTable({
  44. fetchError,
  45. isFetching,
  46. replays,
  47. sort,
  48. visibleColumns,
  49. emptyMessage,
  50. saveLocation,
  51. gridRows,
  52. }: Props) {
  53. const routes = useRoutes();
  54. const newLocation = useLocation();
  55. const organization = useOrganization();
  56. const {
  57. selection: {projects},
  58. } = usePageFilters();
  59. const needSDKUpgrade = useProjectSdkNeedsUpdate({
  60. minVersion: MIN_DEAD_RAGE_CLICK_SDK,
  61. organization,
  62. projectId: projects.map(String),
  63. });
  64. const location: Location = saveLocation
  65. ? {
  66. pathname: '',
  67. search: '',
  68. query: {},
  69. hash: '',
  70. state: '',
  71. action: 'PUSH',
  72. key: '',
  73. }
  74. : newLocation;
  75. const tableHeaders = visibleColumns
  76. .filter(Boolean)
  77. .map(column => <HeaderCell key={column} column={column} sort={sort} />);
  78. if (fetchError && !isFetching) {
  79. return (
  80. <StyledPanelTable
  81. headers={tableHeaders}
  82. isLoading={false}
  83. visibleColumns={visibleColumns}
  84. data-test-id="replay-table"
  85. gridRows={undefined}
  86. >
  87. <StyledAlert type="error" showIcon>
  88. {typeof fetchError === 'string'
  89. ? fetchError
  90. : t(
  91. 'Sorry, the list of replays could not be loaded. This could be due to invalid search parameters or an internal systems error.'
  92. )}
  93. </StyledAlert>
  94. </StyledPanelTable>
  95. );
  96. }
  97. if (
  98. needSDKUpgrade.needsUpdate &&
  99. (visibleColumns.includes(ReplayColumn.MOST_DEAD_CLICKS) ||
  100. visibleColumns.includes(ReplayColumn.MOST_RAGE_CLICKS))
  101. ) {
  102. return (
  103. <StyledPanelTable
  104. headers={tableHeaders}
  105. visibleColumns={visibleColumns}
  106. data-test-id="replay-table"
  107. gridRows={undefined}
  108. loader={<LoadingIndicator style={{margin: '54px auto'}} />}
  109. disablePadding
  110. >
  111. <StyledAlert type="info" showIcon>
  112. {tct('[data] requires [sdkPrompt]. [link:Upgrade now.]', {
  113. data: <strong>Rage and dead clicks</strong>,
  114. sdkPrompt: <strong>{t('SDK version >= 7.60.1')}</strong>,
  115. link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
  116. })}
  117. </StyledAlert>
  118. </StyledPanelTable>
  119. );
  120. }
  121. const referrer = getRouteStringFromRoutes(routes);
  122. const eventView = EventView.fromLocation(location);
  123. return (
  124. <StyledPanelTable
  125. headers={tableHeaders}
  126. isEmpty={replays?.length === 0}
  127. isLoading={isFetching}
  128. visibleColumns={visibleColumns}
  129. disablePadding
  130. data-test-id="replay-table"
  131. emptyMessage={emptyMessage}
  132. gridRows={isFetching ? undefined : gridRows}
  133. loader={<LoadingIndicator style={{margin: '54px auto'}} />}
  134. >
  135. {replays?.map(replay => {
  136. return (
  137. <Fragment key={replay.id}>
  138. {visibleColumns.map(column => {
  139. switch (column) {
  140. case ReplayColumn.ACTIVITY:
  141. return <ActivityCell key="activity" replay={replay} />;
  142. case ReplayColumn.BROWSER:
  143. return <BrowserCell key="browser" replay={replay} />;
  144. case ReplayColumn.COUNT_DEAD_CLICKS:
  145. return <DeadClickCountCell key="countDeadClicks" replay={replay} />;
  146. case ReplayColumn.COUNT_DEAD_CLICKS_NO_HEADER:
  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.COUNT_RAGE_CLICKS_NO_HEADER:
  153. return <RageClickCountCell key="countRageClicks" replay={replay} />;
  154. case ReplayColumn.DURATION:
  155. return <DurationCell key="duration" replay={replay} />;
  156. case ReplayColumn.OS:
  157. return <OSCell key="os" replay={replay} />;
  158. case ReplayColumn.REPLAY:
  159. return (
  160. <ReplayCell
  161. key="session"
  162. replay={replay}
  163. eventView={eventView}
  164. organization={organization}
  165. referrer={referrer}
  166. showUrl
  167. referrer_table="main"
  168. />
  169. );
  170. case ReplayColumn.SLOWEST_TRANSACTION:
  171. return (
  172. <TransactionCell
  173. key="slowestTransaction"
  174. replay={replay}
  175. organization={organization}
  176. />
  177. );
  178. case ReplayColumn.MOST_RAGE_CLICKS:
  179. return (
  180. <ReplayCell
  181. key="mostRageClicks"
  182. replay={replay}
  183. organization={organization}
  184. referrer={referrer}
  185. showUrl={false}
  186. eventView={eventView}
  187. referrer_table="rage-table"
  188. />
  189. );
  190. case ReplayColumn.MOST_DEAD_CLICKS:
  191. return (
  192. <ReplayCell
  193. key="mostDeadClicks"
  194. replay={replay}
  195. organization={organization}
  196. referrer={referrer}
  197. showUrl={false}
  198. eventView={eventView}
  199. referrer_table="dead-table"
  200. />
  201. );
  202. case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
  203. return (
  204. <ReplayCell
  205. key="mostErroneousReplays"
  206. replay={replay}
  207. organization={organization}
  208. referrer={referrer}
  209. showUrl={false}
  210. eventView={eventView}
  211. referrer_table="errors-table"
  212. />
  213. );
  214. default:
  215. return null;
  216. }
  217. })}
  218. </Fragment>
  219. );
  220. })}
  221. </StyledPanelTable>
  222. );
  223. }
  224. const flexibleColumns = [
  225. ReplayColumn.REPLAY,
  226. ReplayColumn.MOST_RAGE_CLICKS,
  227. ReplayColumn.MOST_DEAD_CLICKS,
  228. ReplayColumn.MOST_ERRONEOUS_REPLAYS,
  229. ];
  230. const StyledPanelTable = styled(PanelTable)<{
  231. visibleColumns: ReplayColumn[];
  232. gridRows?: string;
  233. }>`
  234. ${props =>
  235. props.visibleColumns.includes(ReplayColumn.MOST_RAGE_CLICKS) ||
  236. props.visibleColumns.includes(ReplayColumn.MOST_DEAD_CLICKS) ||
  237. props.visibleColumns.includes(ReplayColumn.MOST_ERRONEOUS_REPLAYS)
  238. ? `border-bottom-left-radius: 0; border-bottom-right-radius: 0;`
  239. : ``}
  240. margin-bottom: 0;
  241. grid-template-columns: ${p =>
  242. p.visibleColumns
  243. .filter(Boolean)
  244. .map(column =>
  245. flexibleColumns.includes(column) ? 'minmax(100px, 1fr)' : 'max-content'
  246. )
  247. .join(' ')};
  248. ${props =>
  249. props.gridRows
  250. ? `grid-template-rows: ${props.gridRows};`
  251. : `grid-template-rows: 44px max-content;`}
  252. `;
  253. const StyledAlert = styled(Alert)`
  254. border-radius: 0;
  255. border-width: 1px 0 0 0;
  256. grid-column: 1/-1;
  257. margin-bottom: 0;
  258. `;
  259. export default ReplayTable;