index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import {useCallback, useMemo, useRef, useState} from 'react';
  2. import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualized';
  3. import styled from '@emotion/styled';
  4. import Feature from 'sentry/components/acl/feature';
  5. import Placeholder from 'sentry/components/placeholder';
  6. import {useReplayContext} from 'sentry/components/replays/replayContext';
  7. import {t} from 'sentry/locale';
  8. import {trackAnalytics} from 'sentry/utils/analytics';
  9. import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
  10. import useOrganization from 'sentry/utils/useOrganization';
  11. import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
  12. import useUrlParams from 'sentry/utils/useUrlParams';
  13. import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
  14. import NetworkDetails from 'sentry/views/replays/detail/network/details';
  15. import {ReqRespBodiesAlert} from 'sentry/views/replays/detail/network/details/onboarding';
  16. import NetworkFilters from 'sentry/views/replays/detail/network/networkFilters';
  17. import NetworkHeaderCell, {
  18. COLUMN_COUNT,
  19. } from 'sentry/views/replays/detail/network/networkHeaderCell';
  20. import NetworkTableCell from 'sentry/views/replays/detail/network/networkTableCell';
  21. import useNetworkFilters from 'sentry/views/replays/detail/network/useNetworkFilters';
  22. import useSortNetwork from 'sentry/views/replays/detail/network/useSortNetwork';
  23. import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer';
  24. import useVirtualizedGrid from 'sentry/views/replays/detail/useVirtualizedGrid';
  25. import type {NetworkSpan} from 'sentry/views/replays/types';
  26. const HEADER_HEIGHT = 25;
  27. const BODY_HEIGHT = 28;
  28. const RESIZEABLE_HANDLE_HEIGHT = 90;
  29. type Props = {
  30. isNetworkDetailsSetup: boolean;
  31. networkSpans: undefined | NetworkSpan[];
  32. projectId: undefined | string;
  33. startTimestampMs: number;
  34. };
  35. const cellMeasurer = {
  36. defaultHeight: BODY_HEIGHT,
  37. defaultWidth: 100,
  38. fixedHeight: true,
  39. };
  40. function NetworkList({
  41. isNetworkDetailsSetup,
  42. networkSpans,
  43. projectId,
  44. startTimestampMs,
  45. }: Props) {
  46. const organization = useOrganization();
  47. const {currentTime, currentHoverTime} = useReplayContext();
  48. const [scrollToRow, setScrollToRow] = useState<undefined | number>(undefined);
  49. const filterProps = useNetworkFilters({networkSpans: networkSpans || []});
  50. const {items: filteredItems, searchTerm, setSearchTerm} = filterProps;
  51. const clearSearchTerm = () => setSearchTerm('');
  52. const {handleSort, items, sortConfig} = useSortNetwork({items: filteredItems});
  53. const {handleMouseEnter, handleMouseLeave, handleClick} =
  54. useCrumbHandlers(startTimestampMs);
  55. const containerRef = useRef<HTMLDivElement>(null);
  56. const gridRef = useRef<MultiGrid>(null);
  57. const deps = useMemo(() => [items, searchTerm], [items, searchTerm]);
  58. const {cache, getColumnWidth, onScrollbarPresenceChange, onWrapperResize} =
  59. useVirtualizedGrid({
  60. cellMeasurer,
  61. gridRef,
  62. columnCount: COLUMN_COUNT,
  63. dynamicColumnIndex: 2,
  64. deps,
  65. });
  66. // `initialSize` cannot depend on containerRef because the ref starts as
  67. // `undefined` which then gets set into the hook and doesn't update.
  68. const initialSize = Math.max(150, window.innerHeight * 0.4);
  69. const {size: containerSize, ...resizableDrawerProps} = useResizableDrawer({
  70. direction: 'up',
  71. initialSize,
  72. min: 0,
  73. onResize: () => {},
  74. });
  75. const {getParamValue: getDetailRow, setParamValue: setDetailRow} = useUrlParams(
  76. 'n_detail_row',
  77. ''
  78. );
  79. const detailDataIndex = getDetailRow();
  80. const maxContainerHeight =
  81. (containerRef.current?.clientHeight || window.innerHeight) - RESIZEABLE_HANDLE_HEIGHT;
  82. const splitSize =
  83. networkSpans && detailDataIndex
  84. ? Math.min(maxContainerHeight, containerSize)
  85. : undefined;
  86. const onClickCell = useCallback(
  87. ({dataIndex, rowIndex}: {dataIndex: number; rowIndex: number}) => {
  88. if (getDetailRow() === String(dataIndex)) {
  89. setDetailRow('');
  90. trackAnalytics('replay.details-network-panel-closed', {
  91. is_sdk_setup: isNetworkDetailsSetup,
  92. organization,
  93. });
  94. } else {
  95. setDetailRow(String(dataIndex));
  96. setScrollToRow(rowIndex);
  97. const item = items[dataIndex];
  98. trackAnalytics('replay.details-network-panel-opened', {
  99. is_sdk_setup: isNetworkDetailsSetup,
  100. organization,
  101. resource_method: item.data.method,
  102. resource_status: item.data.statusCode,
  103. resource_type: item.op,
  104. });
  105. }
  106. },
  107. [getDetailRow, isNetworkDetailsSetup, items, organization, setDetailRow]
  108. );
  109. const cellRenderer = ({columnIndex, rowIndex, key, style, parent}: GridCellProps) => {
  110. const network = items[rowIndex - 1];
  111. return (
  112. <CellMeasurer
  113. cache={cache}
  114. columnIndex={columnIndex}
  115. key={key}
  116. parent={parent}
  117. rowIndex={rowIndex}
  118. >
  119. {({
  120. measure: _,
  121. registerChild,
  122. }: {
  123. measure: () => void;
  124. registerChild?: (element?: Element) => void;
  125. }) =>
  126. rowIndex === 0 ? (
  127. <NetworkHeaderCell
  128. ref={e => e && registerChild?.(e)}
  129. handleSort={handleSort}
  130. index={columnIndex}
  131. sortConfig={sortConfig}
  132. style={{...style, height: HEADER_HEIGHT}}
  133. />
  134. ) : (
  135. <NetworkTableCell
  136. columnIndex={columnIndex}
  137. currentHoverTime={currentHoverTime}
  138. currentTime={currentTime}
  139. handleMouseEnter={handleMouseEnter}
  140. handleMouseLeave={handleMouseLeave}
  141. onClickTimestamp={handleClick}
  142. onClickCell={onClickCell}
  143. ref={e => e && registerChild?.(e)}
  144. rowIndex={rowIndex}
  145. sortConfig={sortConfig}
  146. span={network}
  147. startTimestampMs={startTimestampMs}
  148. style={{...style, height: BODY_HEIGHT}}
  149. />
  150. )
  151. }
  152. </CellMeasurer>
  153. );
  154. };
  155. return (
  156. <FluidHeight>
  157. <NetworkFilters networkSpans={networkSpans} {...filterProps} />
  158. <Feature
  159. features={['session-replay-network-details']}
  160. organization={organization}
  161. renderDisabled={false}
  162. >
  163. <ReqRespBodiesAlert isNetworkDetailsSetup={isNetworkDetailsSetup} />
  164. </Feature>
  165. <NetworkTable ref={containerRef}>
  166. <SplitPanel
  167. style={{
  168. gridTemplateRows: splitSize !== undefined ? `1fr auto ${splitSize}px` : '1fr',
  169. }}
  170. >
  171. {networkSpans ? (
  172. <OverflowHidden>
  173. <AutoSizer onResize={onWrapperResize}>
  174. {({height, width}) => (
  175. <MultiGrid
  176. ref={gridRef}
  177. cellRenderer={cellRenderer}
  178. columnCount={COLUMN_COUNT}
  179. columnWidth={getColumnWidth(width)}
  180. deferredMeasurementCache={cache}
  181. estimatedColumnSize={100}
  182. estimatedRowSize={BODY_HEIGHT}
  183. fixedRowCount={1}
  184. height={height}
  185. noContentRenderer={() => (
  186. <NoRowRenderer
  187. unfilteredItems={networkSpans}
  188. clearSearchTerm={clearSearchTerm}
  189. >
  190. {t('No network requests recorded')}
  191. </NoRowRenderer>
  192. )}
  193. onScrollbarPresenceChange={onScrollbarPresenceChange}
  194. onScroll={() => {
  195. if (scrollToRow !== undefined) {
  196. setScrollToRow(undefined);
  197. }
  198. }}
  199. scrollToRow={scrollToRow}
  200. overscanColumnCount={COLUMN_COUNT}
  201. overscanRowCount={5}
  202. rowCount={items.length + 1}
  203. rowHeight={({index}) => (index === 0 ? HEADER_HEIGHT : BODY_HEIGHT)}
  204. width={width}
  205. />
  206. )}
  207. </AutoSizer>
  208. </OverflowHidden>
  209. ) : (
  210. <Placeholder height="100%" />
  211. )}
  212. <Feature
  213. features={['session-replay-network-details']}
  214. organization={organization}
  215. renderDisabled={false}
  216. >
  217. <NetworkDetails
  218. {...resizableDrawerProps}
  219. isSetup={isNetworkDetailsSetup}
  220. item={detailDataIndex ? (items[detailDataIndex] as NetworkSpan) : null}
  221. onClose={() => {
  222. setDetailRow('');
  223. trackAnalytics('replay.details-network-panel-closed', {
  224. is_sdk_setup: isNetworkDetailsSetup,
  225. organization,
  226. });
  227. }}
  228. projectId={projectId}
  229. startTimestampMs={startTimestampMs}
  230. />
  231. </Feature>
  232. </SplitPanel>
  233. </NetworkTable>
  234. </FluidHeight>
  235. );
  236. }
  237. const SplitPanel = styled('div')`
  238. width: 100%;
  239. height: 100%;
  240. position: relative;
  241. display: grid;
  242. overflow: auto;
  243. `;
  244. const OverflowHidden = styled('div')`
  245. position: relative;
  246. height: 100%;
  247. overflow: hidden;
  248. `;
  249. const NetworkTable = styled(FluidHeight)`
  250. border: 1px solid ${p => p.theme.border};
  251. border-radius: ${p => p.theme.borderRadius};
  252. .beforeHoverTime + .afterHoverTime:before {
  253. border-top: 1px solid ${p => p.theme.purple200};
  254. content: '';
  255. left: 0;
  256. position: absolute;
  257. top: 0;
  258. width: 999999999%;
  259. }
  260. .beforeHoverTime:last-child:before {
  261. border-bottom: 1px solid ${p => p.theme.purple200};
  262. content: '';
  263. right: 0;
  264. position: absolute;
  265. bottom: 0;
  266. width: 999999999%;
  267. }
  268. .beforeCurrentTime + .afterCurrentTime:before {
  269. border-top: 1px solid ${p => p.theme.purple300};
  270. content: '';
  271. left: 0;
  272. position: absolute;
  273. top: 0;
  274. width: 999999999%;
  275. }
  276. .beforeCurrentTime:last-child:before {
  277. border-bottom: 1px solid ${p => p.theme.purple300};
  278. content: '';
  279. right: 0;
  280. position: absolute;
  281. bottom: 0;
  282. width: 999999999%;
  283. }
  284. `;
  285. export default NetworkList;