Browse Source

ref(replay): Refactor duplicate code between Network and A11y tabs (#60709)

Ryan Albrecht 1 year ago
parent
commit
8a04d703c1

+ 72 - 0
static/app/components/replays/virtualizedGrid/detailsSplitDivider.tsx

@@ -0,0 +1,72 @@
+import {MouseEvent, ReactNode} from 'react';
+import styled from '@emotion/styled';
+
+import {Button} from 'sentry/components/button';
+import Stacked from 'sentry/components/replays/breadcrumbs/stacked';
+import {IconClose} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
+import SplitDivider from 'sentry/views/replays/detail/layout/splitDivider';
+
+interface Props extends Omit<ReturnType<typeof useResizableDrawer>, 'size'> {
+  onClose: () => void;
+  children?: ReactNode;
+}
+
+export default function DetailsSplitDivider({
+  children,
+  isHeld,
+  onClose,
+  onDoubleClick,
+  onMouseDown,
+}: Props) {
+  return (
+    <StyledStacked>
+      {children}
+      <StyledSplitDivider
+        data-is-held={isHeld}
+        data-slide-direction="updown"
+        onDoubleClick={onDoubleClick}
+        onMouseDown={onMouseDown}
+      />
+      <CloseButtonWrapper>
+        <Button
+          aria-label={t('Hide details')}
+          borderless
+          icon={<IconClose isCircled size="sm" color="subText" />}
+          onClick={(e: MouseEvent) => {
+            e.preventDefault();
+            onClose();
+          }}
+          size="zero"
+        />
+      </CloseButtonWrapper>
+    </StyledStacked>
+  );
+}
+
+const StyledStacked = styled(Stacked)`
+  position: relative;
+  border-top: 1px solid ${p => p.theme.border};
+  border-bottom: 1px solid ${p => p.theme.border};
+`;
+
+const CloseButtonWrapper = styled('div')`
+  position: absolute;
+  right: 0;
+  height: 100%;
+  padding: ${space(1)};
+  z-index: ${p => p.theme.zIndex.initial};
+  display: flex;
+  align-items: center;
+`;
+
+const StyledSplitDivider = styled(SplitDivider)`
+  padding: ${space(0.75)};
+
+  :hover,
+  &[data-is-held='true'] {
+    z-index: ${p => p.theme.zIndex.initial};
+  }
+`;

+ 44 - 0
static/app/components/replays/virtualizedGrid/gridTable.tsx

@@ -0,0 +1,44 @@
+import styled from '@emotion/styled';
+
+import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
+
+export const GridTable = 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%;
+  }
+`;

+ 8 - 0
static/app/components/replays/virtualizedGrid/overflowHidden.tsx

@@ -0,0 +1,8 @@
+import styled from '@emotion/styled';
+
+export const OverflowHidden = styled('div')`
+  position: relative;
+  height: 100%;
+  overflow: hidden;
+  display: grid;
+`;

+ 10 - 0
static/app/components/replays/virtualizedGrid/splitPanel.tsx

@@ -0,0 +1,10 @@
+import styled from '@emotion/styled';
+
+export const SplitPanel = styled('div')`
+  width: 100%;
+  height: 100%;
+
+  position: relative;
+  display: grid;
+  overflow: auto;
+`;

+ 74 - 0
static/app/components/replays/virtualizedGrid/useDetailsSplit.tsx

@@ -0,0 +1,74 @@
+import {RefObject, useCallback} from 'react';
+
+import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
+import useUrlParams from 'sentry/utils/useUrlParams';
+
+interface OnClickProps {
+  dataIndex: number;
+  rowIndex: number;
+}
+
+interface Props {
+  containerRef: RefObject<HTMLDivElement>;
+  frames: undefined | ReadonlyArray<unknown>;
+  handleHeight: number;
+  urlParamName: string;
+  onHideDetails?: () => void;
+  onShowDetails?: (props: OnClickProps) => void;
+}
+
+export default function useDetailsSplit({
+  containerRef,
+  frames,
+  handleHeight,
+  onHideDetails,
+  onShowDetails,
+  urlParamName,
+}: Props) {
+  const {getParamValue: getDetailIndex, setParamValue: setDetailIndex} = useUrlParams(
+    urlParamName,
+    ''
+  );
+
+  const onClickCell = useCallback(
+    ({dataIndex, rowIndex}: OnClickProps) => {
+      if (getDetailIndex() === String(dataIndex)) {
+        setDetailIndex('');
+        onHideDetails?.();
+      } else {
+        setDetailIndex(String(dataIndex));
+        onShowDetails?.({dataIndex, rowIndex});
+      }
+    },
+    [getDetailIndex, setDetailIndex, onHideDetails, onShowDetails]
+  );
+
+  const onCloseDetailsSplit = useCallback(() => {
+    setDetailIndex('');
+    onHideDetails?.();
+  }, [setDetailIndex, onHideDetails]);
+
+  // `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 maxContainerHeight =
+    (containerRef.current?.clientHeight || window.innerHeight) - handleHeight;
+  const splitSize =
+    frames && getDetailIndex() ? Math.min(maxContainerHeight, containerSize) : undefined;
+
+  return {
+    onClickCell,
+    onCloseDetailsSplit,
+    resizableDrawerProps,
+    selectedIndex: getDetailIndex(),
+    splitSize,
+  };
+}

+ 8 - 53
static/app/views/replays/detail/accessibility/details/index.tsx

@@ -1,15 +1,9 @@
-import {Fragment, MouseEvent} from 'react';
-import styled from '@emotion/styled';
+import {Fragment} from 'react';
 
-import {Button} from 'sentry/components/button';
-import Stacked from 'sentry/components/replays/breadcrumbs/stacked';
-import {IconClose} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
+import DetailsSplitDivider from 'sentry/components/replays/virtualizedGrid/detailsSplitDivider';
 import type {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
 import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
 import AccessibilityDetailsContent from 'sentry/views/replays/detail/accessibility/details/content';
-import SplitDivider from 'sentry/views/replays/detail/layout/splitDivider';
 
 type Props = {
   item: null | HydratedA11yFrame;
@@ -29,55 +23,16 @@ function AccessibilityDetails({
 
   return (
     <Fragment>
-      <StyledStacked>
-        <StyledSplitDivider
-          data-is-held={isHeld}
-          data-slide-direction="updown"
-          onDoubleClick={onDoubleClick}
-          onMouseDown={onMouseDown}
-        />
-        <CloseButtonWrapper>
-          <Button
-            aria-label={t('Hide accessibility details')}
-            borderless
-            icon={<IconClose isCircled size="sm" color="subText" />}
-            onClick={(e: MouseEvent) => {
-              e.preventDefault();
-              onClose();
-            }}
-            size="zero"
-          />
-        </CloseButtonWrapper>
-      </StyledStacked>
+      <DetailsSplitDivider
+        isHeld={isHeld}
+        onClose={onClose}
+        onDoubleClick={onDoubleClick}
+        onMouseDown={onMouseDown}
+      />
 
       <AccessibilityDetailsContent item={item} />
     </Fragment>
   );
 }
 
-const StyledStacked = styled(Stacked)`
-  position: relative;
-  border-top: 1px solid ${p => p.theme.border};
-  border-bottom: 1px solid ${p => p.theme.border};
-`;
-
-const CloseButtonWrapper = styled('div')`
-  position: absolute;
-  right: 0;
-  height: 100%;
-  padding: ${space(1)};
-  z-index: ${p => p.theme.zIndex.initial};
-  display: flex;
-  align-items: center;
-`;
-
-const StyledSplitDivider = styled(SplitDivider)`
-  padding: ${space(0.75)};
-
-  :hover,
-  &[data-is-held='true'] {
-    z-index: ${p => p.theme.zIndex.initial};
-  }
-`;
-
 export default AccessibilityDetails;

+ 23 - 109
static/app/views/replays/detail/accessibility/index.tsx

@@ -1,16 +1,17 @@
 import {useCallback, useMemo, useRef, useState} from 'react';
 import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualized';
-import styled from '@emotion/styled';
 
 import Placeholder from 'sentry/components/placeholder';
 import JumpButtons from 'sentry/components/replays/jumpButtons';
 import {useReplayContext} from 'sentry/components/replays/replayContext';
 import useJumpButtons from 'sentry/components/replays/useJumpButtons';
+import {GridTable} from 'sentry/components/replays/virtualizedGrid/gridTable';
+import {OverflowHidden} from 'sentry/components/replays/virtualizedGrid/overflowHidden';
+import {SplitPanel} from 'sentry/components/replays/virtualizedGrid/splitPanel';
+import useDetailsSplit from 'sentry/components/replays/virtualizedGrid/useDetailsSplit';
 import {t} from 'sentry/locale';
 import useA11yData from 'sentry/utils/replays/hooks/useA11yData';
 import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
-import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
-import useUrlParams from 'sentry/utils/useUrlParams';
 import AccessibilityFilters from 'sentry/views/replays/detail/accessibility/accessibilityFilters';
 import AccessibilityHeaderCell, {
   COLUMN_COUNT,
@@ -62,28 +63,21 @@ function AccessibilityList() {
       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 {
+    onClickCell,
+    onCloseDetailsSplit,
+    resizableDrawerProps,
+    selectedIndex,
+    splitSize,
+  } = useDetailsSplit({
+    containerRef,
+    handleHeight: RESIZEABLE_HANDLE_HEIGHT,
+    frames: accessibilityData,
+    urlParamName: 'a_detail_row',
+    onShowDetails: useCallback(({rowIndex}) => {
+      setScrollToRow(rowIndex);
+    }, []),
   });
-  const {getParamValue: getDetailRow, setParamValue: setDetailRow} = useUrlParams(
-    'a_detail_row',
-    ''
-  );
-  const detailDataIndex = getDetailRow();
-
-  const maxContainerHeight =
-    (containerRef.current?.clientHeight || window.innerHeight) - RESIZEABLE_HANDLE_HEIGHT;
-  const splitSize =
-    accessibilityData && detailDataIndex
-      ? Math.min(maxContainerHeight, containerSize)
-      : undefined;
 
   const {
     handleClick: onClickToJump,
@@ -97,18 +91,6 @@ function AccessibilityList() {
     setScrollToRow,
   });
 
-  const onClickCell = useCallback(
-    ({dataIndex, rowIndex}: {dataIndex: number; rowIndex: number}) => {
-      if (getDetailRow() === String(dataIndex)) {
-        setDetailRow('');
-      } else {
-        setDetailRow(String(dataIndex));
-        setScrollToRow(rowIndex);
-      }
-    },
-    [getDetailRow, setDetailRow]
-  );
-
   const cellRenderer = ({columnIndex, rowIndex, key, style, parent}: GridCellProps) => {
     const a11yIssue = items[rowIndex - 1];
 
@@ -120,13 +102,7 @@ function AccessibilityList() {
         parent={parent}
         rowIndex={rowIndex}
       >
-        {({
-          measure: _,
-          registerChild,
-        }: {
-          measure: () => void;
-          registerChild?: (element?: Element) => void;
-        }) =>
+        {({measure: _, registerChild}) =>
           rowIndex === 0 ? (
             <AccessibilityHeaderCell
               ref={e => e && registerChild?.(e)}
@@ -161,10 +137,7 @@ function AccessibilityList() {
       <FilterLoadingIndicator isLoading={isLoading}>
         <AccessibilityFilters accessibilityData={accessibilityData} {...filterProps} />
       </FilterLoadingIndicator>
-      <AccessibilityTable
-        ref={containerRef}
-        data-test-id="replay-details-accessibility-tab"
-      >
+      <GridTable ref={containerRef} data-test-id="replay-details-accessibility-tab">
         <SplitPanel
           style={{
             gridTemplateRows: splitSize !== undefined ? `1fr auto ${splitSize}px` : '1fr',
@@ -221,72 +194,13 @@ function AccessibilityList() {
           )}
           <AccessibilityDetails
             {...resizableDrawerProps}
-            item={detailDataIndex ? items[detailDataIndex] : null}
-            onClose={() => {
-              setDetailRow('');
-            }}
+            item={selectedIndex ? items[selectedIndex] : null}
+            onClose={onCloseDetailsSplit}
           />
         </SplitPanel>
-      </AccessibilityTable>
+      </GridTable>
     </FluidHeight>
   );
 }
 
-const SplitPanel = styled('div')`
-  width: 100%;
-  height: 100%;
-
-  position: relative;
-  display: grid;
-  overflow: auto;
-`;
-
-const OverflowHidden = styled('div')`
-  position: relative;
-  height: 100%;
-  overflow: hidden;
-  display: grid;
-`;
-
-const AccessibilityTable = 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 AccessibilityList;

+ 10 - 81
static/app/views/replays/detail/network/details/index.tsx

@@ -1,15 +1,9 @@
-import {Fragment, MouseEvent} from 'react';
-import styled from '@emotion/styled';
+import {Fragment} from 'react';
 
-import {Button} from 'sentry/components/button';
-import Stacked from 'sentry/components/replays/breadcrumbs/stacked';
-import {IconClose} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
+import DetailsSplitDivider from 'sentry/components/replays/virtualizedGrid/detailsSplitDivider';
 import type {SpanFrame} from 'sentry/utils/replays/types';
 import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
 import useUrlParams from 'sentry/utils/useUrlParams';
-import SplitDivider from 'sentry/views/replays/detail/layout/splitDivider';
 import NetworkDetailsContent from 'sentry/views/replays/detail/network/details/content';
 import NetworkDetailsTabs, {
   TabKey,
@@ -43,27 +37,14 @@ function NetworkDetails({
 
   return (
     <Fragment>
-      <StyledStacked>
-        <StyledNetworkDetailsTabs underlined={false} />
-        <StyledSplitDivider
-          data-is-held={isHeld}
-          data-slide-direction="updown"
-          onDoubleClick={onDoubleClick}
-          onMouseDown={onMouseDown}
-        />
-        <CloseButtonWrapper>
-          <Button
-            aria-label={t('Hide request details')}
-            borderless
-            icon={<IconClose isCircled size="sm" color="subText" />}
-            onClick={(e: MouseEvent) => {
-              e.preventDefault();
-              onClose();
-            }}
-            size="zero"
-          />
-        </CloseButtonWrapper>
-      </StyledStacked>
+      <DetailsSplitDivider
+        isHeld={isHeld}
+        onClose={onClose}
+        onDoubleClick={onDoubleClick}
+        onMouseDown={onMouseDown}
+      >
+        <NetworkDetailsTabs underlined={false} />
+      </DetailsSplitDivider>
 
       <NetworkDetailsContent
         isSetup={isSetup}
@@ -76,56 +57,4 @@ function NetworkDetails({
   );
 }
 
-const StyledStacked = styled(Stacked)`
-  position: relative;
-  border-top: 1px solid ${p => p.theme.border};
-  border-bottom: 1px solid ${p => p.theme.border};
-`;
-
-const StyledNetworkDetailsTabs = styled(NetworkDetailsTabs)`
-  /*
-  Use padding instead of margin so all the <li> will cover the <SplitDivider>
-  without taking 100% width.
-  */
-
-  & > li {
-    margin-right: 0;
-    padding-right: ${space(3)};
-    background: ${p => p.theme.surface400};
-    z-index: ${p => p.theme.zIndex.initial};
-  }
-  & > li:first-child {
-    padding-left: ${space(2)};
-  }
-  & > li:last-child {
-    padding-right: ${space(1)};
-  }
-
-  & > li > a {
-    padding-top: ${space(1)};
-    padding-bottom: ${space(0.5)};
-    height: 100%;
-    border-bottom: ${space(0.5)} solid transparent;
-  }
-`;
-
-const CloseButtonWrapper = styled('div')`
-  position: absolute;
-  right: 0;
-  height: 100%;
-  padding: ${space(1)};
-  z-index: ${p => p.theme.zIndex.initial};
-  display: flex;
-  align-items: center;
-`;
-
-const StyledSplitDivider = styled(SplitDivider)`
-  padding: ${space(0.75)};
-
-  :hover,
-  &[data-is-held='true'] {
-    z-index: ${p => p.theme.zIndex.initial};
-  }
-`;
-
 export default NetworkDetails;

+ 31 - 2
static/app/views/replays/detail/network/details/tabs.tsx

@@ -1,8 +1,10 @@
+import styled from '@emotion/styled';
 import queryString from 'query-string';
 
 import ListLink from 'sentry/components/links/listLink';
 import ScrollableTabs from 'sentry/components/replays/scrollableTabs';
 import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
 import {useLocation} from 'sentry/utils/useLocation';
 import useUrlParams from 'sentry/utils/useUrlParams';
 
@@ -19,7 +21,7 @@ const TABS = {
 
 export type TabKey = keyof typeof TABS;
 
-function NetworkRequestTabs({className, underlined = true}: Props) {
+function NetworkDetailsTabs({className, underlined = true}: Props) {
   const {pathname, query} = useLocation();
   const {getParamValue, setParamValue} = useUrlParams('n_detail_tab', 'details');
   const activeTab = getParamValue();
@@ -43,4 +45,31 @@ function NetworkRequestTabs({className, underlined = true}: Props) {
   );
 }
 
-export default NetworkRequestTabs;
+const StyledNetworkDetailsTabs = styled(NetworkDetailsTabs)`
+  /*
+  Use padding instead of margin so all the <li> will cover the <SplitDivider>
+  without taking 100% width.
+  */
+
+  & > li {
+    margin-right: 0;
+    padding-right: ${space(3)};
+    background: ${p => p.theme.surface400};
+    z-index: ${p => p.theme.zIndex.initial};
+  }
+  & > li:first-child {
+    padding-left: ${space(2)};
+  }
+  & > li:last-child {
+    padding-right: ${space(1)};
+  }
+
+  & > li > a {
+    padding-top: ${space(1)};
+    padding-bottom: ${space(0.5)};
+    height: 100%;
+    border-bottom: ${space(0.5)} solid transparent;
+  }
+`;
+
+export default StyledNetworkDetailsTabs;

+ 44 - 124
static/app/views/replays/detail/network/index.tsx

@@ -1,18 +1,20 @@
 import {useCallback, useMemo, useRef, useState} from 'react';
 import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualized';
-import styled from '@emotion/styled';
 
 import Placeholder from 'sentry/components/placeholder';
 import JumpButtons from 'sentry/components/replays/jumpButtons';
 import {useReplayContext} from 'sentry/components/replays/replayContext';
 import useJumpButtons from 'sentry/components/replays/useJumpButtons';
+import {GridTable} from 'sentry/components/replays/virtualizedGrid/gridTable';
+import {OverflowHidden} from 'sentry/components/replays/virtualizedGrid/overflowHidden';
+import {SplitPanel} from 'sentry/components/replays/virtualizedGrid/splitPanel';
+import useDetailsSplit from 'sentry/components/replays/virtualizedGrid/useDetailsSplit';
 import {t} from 'sentry/locale';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
 import {getFrameMethod, getFrameStatus} from 'sentry/utils/replays/resourceFrame';
 import useOrganization from 'sentry/utils/useOrganization';
-import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
-import useUrlParams from 'sentry/utils/useUrlParams';
+import FilterLoadingIndicator from 'sentry/views/replays/detail/filterLoadingIndicator';
 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';
@@ -66,28 +68,39 @@ function NetworkList() {
       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 {
+    onClickCell,
+    onCloseDetailsSplit,
+    resizableDrawerProps,
+    selectedIndex,
+    splitSize,
+  } = useDetailsSplit({
+    containerRef,
+    frames: networkFrames,
+    handleHeight: RESIZEABLE_HANDLE_HEIGHT,
+    urlParamName: 'n_detail_row',
+    onShowDetails: useCallback(
+      ({dataIndex, rowIndex}) => {
+        setScrollToRow(rowIndex);
 
-  const {size: containerSize, ...resizableDrawerProps} = useResizableDrawer({
-    direction: 'up',
-    initialSize,
-    min: 0,
-    onResize: () => {},
+        const item = items[dataIndex];
+        trackAnalytics('replay.details-network-panel-opened', {
+          is_sdk_setup: isNetworkDetailsSetup,
+          organization,
+          resource_method: getFrameMethod(item),
+          resource_status: String(getFrameStatus(item)),
+          resource_type: item.op,
+        });
+      },
+      [organization, items, isNetworkDetailsSetup]
+    ),
+    onHideDetails: useCallback(() => {
+      trackAnalytics('replay.details-network-panel-closed', {
+        is_sdk_setup: isNetworkDetailsSetup,
+        organization,
+      });
+    }, [organization, isNetworkDetailsSetup]),
   });
-  const {getParamValue: getDetailRow, setParamValue: setDetailRow} = useUrlParams(
-    'n_detail_row',
-    ''
-  );
-  const detailDataIndex = getDetailRow();
-
-  const maxContainerHeight =
-    (containerRef.current?.clientHeight || window.innerHeight) - RESIZEABLE_HANDLE_HEIGHT;
-  const splitSize =
-    networkFrames && detailDataIndex
-      ? Math.min(maxContainerHeight, containerSize)
-      : undefined;
 
   const {
     handleClick: onClickToJump,
@@ -101,32 +114,6 @@ function NetworkList() {
     setScrollToRow,
   });
 
-  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: getFrameMethod(item),
-          resource_status: String(getFrameStatus(item)),
-          resource_type: item.op,
-        });
-      }
-    },
-    [getDetailRow, isNetworkDetailsSetup, items, organization, setDetailRow]
-  );
-
   const cellRenderer = ({columnIndex, rowIndex, key, style, parent}: GridCellProps) => {
     const network = items[rowIndex - 1];
 
@@ -138,13 +125,7 @@ function NetworkList() {
         parent={parent}
         rowIndex={rowIndex}
       >
-        {({
-          measure: _,
-          registerChild,
-        }: {
-          measure: () => void;
-          registerChild?: (element?: Element) => void;
-        }) =>
+        {({measure: _, registerChild}) =>
           rowIndex === 0 ? (
             <NetworkHeaderCell
               ref={e => e && registerChild?.(e)}
@@ -177,9 +158,11 @@ function NetworkList() {
 
   return (
     <FluidHeight>
-      <NetworkFilters networkFrames={networkFrames} {...filterProps} />
+      <FilterLoadingIndicator isLoading={!replay}>
+        <NetworkFilters networkFrames={networkFrames} {...filterProps} />
+      </FilterLoadingIndicator>
       <ReqRespBodiesAlert isNetworkDetailsSetup={isNetworkDetailsSetup} />
-      <NetworkTable ref={containerRef} data-test-id="replay-details-network-tab">
+      <GridTable ref={containerRef} data-test-id="replay-details-network-tab">
         <SplitPanel
           style={{
             gridTemplateRows: splitSize !== undefined ? `1fr auto ${splitSize}px` : '1fr',
@@ -237,78 +220,15 @@ function NetworkList() {
           <NetworkDetails
             {...resizableDrawerProps}
             isSetup={isNetworkDetailsSetup}
-            item={detailDataIndex ? items[detailDataIndex] : null}
-            onClose={() => {
-              setDetailRow('');
-              trackAnalytics('replay.details-network-panel-closed', {
-                is_sdk_setup: isNetworkDetailsSetup,
-                organization,
-              });
-            }}
+            item={selectedIndex ? items[selectedIndex] : null}
+            onClose={onCloseDetailsSplit}
             projectId={projectId}
             startTimestampMs={startTimestampMs}
           />
         </SplitPanel>
-      </NetworkTable>
+      </GridTable>
     </FluidHeight>
   );
 }
 
-const SplitPanel = styled('div')`
-  width: 100%;
-  height: 100%;
-
-  position: relative;
-  display: grid;
-  overflow: auto;
-`;
-
-const OverflowHidden = styled('div')`
-  position: relative;
-  height: 100%;
-  overflow: hidden;
-  display: grid;
-`;
-
-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;