import {useCallback, useMemo, useRef, useState} from 'react'; import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualized'; import styled from '@emotion/styled'; import Feature from 'sentry/components/acl/feature'; import Placeholder from 'sentry/components/placeholder'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; import useOrganization from 'sentry/utils/useOrganization'; import {useResizableDrawer} from 'sentry/utils/useResizableDrawer'; import useUrlParams from 'sentry/utils/useUrlParams'; import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight'; import NetworkDetails from 'sentry/views/replays/detail/network/details'; import {ReqRespBodiesAlert} from 'sentry/views/replays/detail/network/details/onboarding'; import NetworkFilters from 'sentry/views/replays/detail/network/networkFilters'; import NetworkHeaderCell, { COLUMN_COUNT, } from 'sentry/views/replays/detail/network/networkHeaderCell'; import NetworkTableCell from 'sentry/views/replays/detail/network/networkTableCell'; import useNetworkFilters from 'sentry/views/replays/detail/network/useNetworkFilters'; import useSortNetwork from 'sentry/views/replays/detail/network/useSortNetwork'; import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer'; import useVirtualizedGrid from 'sentry/views/replays/detail/useVirtualizedGrid'; import type {NetworkSpan} from 'sentry/views/replays/types'; const HEADER_HEIGHT = 25; const BODY_HEIGHT = 28; const RESIZEABLE_HANDLE_HEIGHT = 90; type Props = { isNetworkDetailsSetup: boolean; networkSpans: undefined | NetworkSpan[]; projectId: undefined | string; startTimestampMs: number; }; const cellMeasurer = { defaultHeight: BODY_HEIGHT, defaultWidth: 100, fixedHeight: true, }; function NetworkList({ isNetworkDetailsSetup, networkSpans, projectId, startTimestampMs, }: Props) { const organization = useOrganization(); const {currentTime, currentHoverTime} = useReplayContext(); const [scrollToRow, setScrollToRow] = useState(undefined); const filterProps = useNetworkFilters({networkSpans: networkSpans || []}); const {items: filteredItems, searchTerm, setSearchTerm} = filterProps; const clearSearchTerm = () => setSearchTerm(''); const {handleSort, items, sortConfig} = useSortNetwork({items: filteredItems}); const {handleMouseEnter, handleMouseLeave, handleClick} = useCrumbHandlers(startTimestampMs); const containerRef = useRef(null); const gridRef = useRef(null); const deps = useMemo(() => [items, searchTerm], [items, searchTerm]); const {cache, getColumnWidth, onScrollbarPresenceChange, onWrapperResize} = useVirtualizedGrid({ cellMeasurer, gridRef, columnCount: COLUMN_COUNT, dynamicColumnIndex: 2, deps, }); // `initialSize` cannot depend on containerRef because the ref starts as // `undefined` which then gets set into the hook and doesn't update. const initialSize = Math.max(150, window.innerHeight * 0.4); const {size: containerSize, ...resizableDrawerProps} = useResizableDrawer({ direction: 'up', initialSize, min: 0, onResize: () => {}, }); const {getParamValue: getDetailRow, setParamValue: setDetailRow} = useUrlParams( 'n_detail_row', '' ); const detailDataIndex = getDetailRow(); const maxContainerHeight = (containerRef.current?.clientHeight || window.innerHeight) - RESIZEABLE_HANDLE_HEIGHT; const splitSize = networkSpans && detailDataIndex ? Math.min(maxContainerHeight, containerSize) : undefined; const onClickCell = useCallback( ({dataIndex, rowIndex}: {dataIndex: number; rowIndex: number}) => { if (getDetailRow() === String(dataIndex)) { setDetailRow(''); trackAnalytics('replay.details-network-panel-closed', { is_sdk_setup: isNetworkDetailsSetup, organization, }); } else { setDetailRow(String(dataIndex)); setScrollToRow(rowIndex); const item = items[dataIndex]; trackAnalytics('replay.details-network-panel-opened', { is_sdk_setup: isNetworkDetailsSetup, organization, resource_method: item.data.method, resource_status: item.data.statusCode, resource_type: item.op, }); } }, [getDetailRow, isNetworkDetailsSetup, items, organization, setDetailRow] ); const cellRenderer = ({columnIndex, rowIndex, key, style, parent}: GridCellProps) => { const network = items[rowIndex - 1]; return ( {({ measure: _, registerChild, }: { measure: () => void; registerChild?: (element?: Element) => void; }) => rowIndex === 0 ? ( e && registerChild?.(e)} handleSort={handleSort} index={columnIndex} sortConfig={sortConfig} style={{...style, height: HEADER_HEIGHT}} /> ) : ( e && registerChild?.(e)} rowIndex={rowIndex} sortConfig={sortConfig} span={network} startTimestampMs={startTimestampMs} style={{...style, height: BODY_HEIGHT}} /> ) } ); }; return ( {networkSpans ? ( {({height, width}) => ( ( {t('No network requests recorded')} )} onScrollbarPresenceChange={onScrollbarPresenceChange} onScroll={() => { if (scrollToRow !== undefined) { setScrollToRow(undefined); } }} scrollToRow={scrollToRow} overscanColumnCount={COLUMN_COUNT} overscanRowCount={5} rowCount={items.length + 1} rowHeight={({index}) => (index === 0 ? HEADER_HEIGHT : BODY_HEIGHT)} width={width} /> )} ) : ( )} { setDetailRow(''); trackAnalytics('replay.details-network-panel-closed', { is_sdk_setup: isNetworkDetailsSetup, organization, }); }} projectId={projectId} startTimestampMs={startTimestampMs} /> ); } const SplitPanel = styled('div')` width: 100%; height: 100%; position: relative; display: grid; overflow: auto; `; const OverflowHidden = styled('div')` position: relative; height: 100%; overflow: hidden; `; const NetworkTable = styled(FluidHeight)` border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; .beforeHoverTime + .afterHoverTime:before { border-top: 1px solid ${p => p.theme.purple200}; content: ''; left: 0; position: absolute; top: 0; width: 999999999%; } .beforeHoverTime:last-child:before { border-bottom: 1px solid ${p => p.theme.purple200}; content: ''; right: 0; position: absolute; bottom: 0; width: 999999999%; } .beforeCurrentTime + .afterCurrentTime:before { border-top: 1px solid ${p => p.theme.purple300}; content: ''; left: 0; position: absolute; top: 0; width: 999999999%; } .beforeCurrentTime:last-child:before { border-bottom: 1px solid ${p => p.theme.purple300}; content: ''; right: 0; position: absolute; bottom: 0; width: 999999999%; } `; export default NetworkList;