index.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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 Placeholder from 'sentry/components/placeholder';
  5. import {useReplayContext} from 'sentry/components/replays/replayContext';
  6. import {t} from 'sentry/locale';
  7. import {trackAnalytics} from 'sentry/utils/analytics';
  8. import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
  9. import {getFrameMethod, getFrameStatus} from 'sentry/utils/replays/resourceFrame';
  10. import type {SpanFrame} from 'sentry/utils/replays/types';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
  13. import useUrlParams from 'sentry/utils/useUrlParams';
  14. import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
  15. import NetworkDetails from 'sentry/views/replays/detail/network/details';
  16. import {ReqRespBodiesAlert} from 'sentry/views/replays/detail/network/details/onboarding';
  17. import NetworkFilters from 'sentry/views/replays/detail/network/networkFilters';
  18. import NetworkHeaderCell, {
  19. COLUMN_COUNT,
  20. } from 'sentry/views/replays/detail/network/networkHeaderCell';
  21. import NetworkTableCell from 'sentry/views/replays/detail/network/networkTableCell';
  22. import useNetworkFilters from 'sentry/views/replays/detail/network/useNetworkFilters';
  23. import useSortNetwork from 'sentry/views/replays/detail/network/useSortNetwork';
  24. import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer';
  25. import useVirtualizedGrid from 'sentry/views/replays/detail/useVirtualizedGrid';
  26. const HEADER_HEIGHT = 25;
  27. const BODY_HEIGHT = 28;
  28. const RESIZEABLE_HANDLE_HEIGHT = 90;
  29. type Props = {
  30. isNetworkDetailsSetup: boolean;
  31. networkFrames: undefined | SpanFrame[];
  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. networkFrames,
  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({networkFrames: networkFrames || []});
  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. networkFrames && 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: getFrameMethod(item),
  102. resource_status: String(getFrameStatus(item)),
  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. onMouseEnter={handleMouseEnter}
  140. onMouseLeave={handleMouseLeave}
  141. onClickTimestamp={handleClick}
  142. onClickCell={onClickCell}
  143. ref={e => e && registerChild?.(e)}
  144. rowIndex={rowIndex}
  145. sortConfig={sortConfig}
  146. frame={network}
  147. startTimestampMs={startTimestampMs}
  148. style={{...style, height: BODY_HEIGHT}}
  149. />
  150. )
  151. }
  152. </CellMeasurer>
  153. );
  154. };
  155. return (
  156. <FluidHeight>
  157. <NetworkFilters networkFrames={networkFrames} {...filterProps} />
  158. <ReqRespBodiesAlert isNetworkDetailsSetup={isNetworkDetailsSetup} />
  159. <NetworkTable ref={containerRef} data-test-id="replay-details-network-tab">
  160. <SplitPanel
  161. style={{
  162. gridTemplateRows: splitSize !== undefined ? `1fr auto ${splitSize}px` : '1fr',
  163. }}
  164. >
  165. {networkFrames ? (
  166. <OverflowHidden>
  167. <AutoSizer onResize={onWrapperResize}>
  168. {({height, width}) => (
  169. <MultiGrid
  170. ref={gridRef}
  171. cellRenderer={cellRenderer}
  172. columnCount={COLUMN_COUNT}
  173. columnWidth={getColumnWidth(width)}
  174. deferredMeasurementCache={cache}
  175. estimatedColumnSize={100}
  176. estimatedRowSize={BODY_HEIGHT}
  177. fixedRowCount={1}
  178. height={height}
  179. noContentRenderer={() => (
  180. <NoRowRenderer
  181. unfilteredItems={networkFrames}
  182. clearSearchTerm={clearSearchTerm}
  183. >
  184. {t('No network requests recorded')}
  185. </NoRowRenderer>
  186. )}
  187. onScrollbarPresenceChange={onScrollbarPresenceChange}
  188. onScroll={() => {
  189. if (scrollToRow !== undefined) {
  190. setScrollToRow(undefined);
  191. }
  192. }}
  193. scrollToRow={scrollToRow}
  194. overscanColumnCount={COLUMN_COUNT}
  195. overscanRowCount={5}
  196. rowCount={items.length + 1}
  197. rowHeight={({index}) => (index === 0 ? HEADER_HEIGHT : BODY_HEIGHT)}
  198. width={width}
  199. />
  200. )}
  201. </AutoSizer>
  202. </OverflowHidden>
  203. ) : (
  204. <Placeholder height="100%" />
  205. )}
  206. <NetworkDetails
  207. {...resizableDrawerProps}
  208. isSetup={isNetworkDetailsSetup}
  209. item={detailDataIndex ? items[detailDataIndex] : null}
  210. onClose={() => {
  211. setDetailRow('');
  212. trackAnalytics('replay.details-network-panel-closed', {
  213. is_sdk_setup: isNetworkDetailsSetup,
  214. organization,
  215. });
  216. }}
  217. projectId={projectId}
  218. startTimestampMs={startTimestampMs}
  219. />
  220. </SplitPanel>
  221. </NetworkTable>
  222. </FluidHeight>
  223. );
  224. }
  225. const SplitPanel = styled('div')`
  226. width: 100%;
  227. height: 100%;
  228. position: relative;
  229. display: grid;
  230. overflow: auto;
  231. `;
  232. const OverflowHidden = styled('div')`
  233. position: relative;
  234. height: 100%;
  235. overflow: hidden;
  236. `;
  237. const NetworkTable = styled(FluidHeight)`
  238. border: 1px solid ${p => p.theme.border};
  239. border-radius: ${p => p.theme.borderRadius};
  240. .beforeHoverTime + .afterHoverTime:before {
  241. border-top: 1px solid ${p => p.theme.purple200};
  242. content: '';
  243. left: 0;
  244. position: absolute;
  245. top: 0;
  246. width: 999999999%;
  247. }
  248. .beforeHoverTime:last-child:before {
  249. border-bottom: 1px solid ${p => p.theme.purple200};
  250. content: '';
  251. right: 0;
  252. position: absolute;
  253. bottom: 0;
  254. width: 999999999%;
  255. }
  256. .beforeCurrentTime + .afterCurrentTime:before {
  257. border-top: 1px solid ${p => p.theme.purple300};
  258. content: '';
  259. left: 0;
  260. position: absolute;
  261. top: 0;
  262. width: 999999999%;
  263. }
  264. .beforeCurrentTime:last-child:before {
  265. border-bottom: 1px solid ${p => p.theme.purple300};
  266. content: '';
  267. right: 0;
  268. position: absolute;
  269. bottom: 0;
  270. width: 999999999%;
  271. }
  272. `;
  273. export default NetworkList;