import {CSSProperties, forwardRef, MouseEvent, useMemo} from 'react'; import styled from '@emotion/styled'; import classNames from 'classnames'; import FileSize from 'sentry/components/fileSize'; import {relativeTimeInMs} from 'sentry/components/replays/utils'; import {Tooltip} from 'sentry/components/tooltip'; import {space} from 'sentry/styles/space'; import useUrlParams from 'sentry/utils/useUrlParams'; import useSortNetwork from 'sentry/views/replays/detail/network/useSortNetwork'; import TimestampButton from 'sentry/views/replays/detail/timestampButton'; import {operationName} from 'sentry/views/replays/detail/utils'; import type {NetworkSpan} from 'sentry/views/replays/types'; const EMPTY_CELL = '--'; type Props = { columnIndex: number; currentHoverTime: number | undefined; currentTime: number; handleMouseEnter: (span: NetworkSpan) => void; handleMouseLeave: (span: NetworkSpan) => void; onClickCell: (props: {dataIndex: number; rowIndex: number}) => void; onClickTimestamp: (crumb: NetworkSpan) => void; rowIndex: number; sortConfig: ReturnType['sortConfig']; span: NetworkSpan; startTimestampMs: number; style: CSSProperties; }; type CellProps = { hasOccurred: boolean | undefined; isDetailsOpen: boolean; isStatusError: boolean; className?: string; numeric?: boolean; onClick?: undefined | (() => void); }; const NetworkTableCell = forwardRef( ( { columnIndex, currentHoverTime, currentTime, handleMouseEnter, handleMouseLeave, onClickCell, onClickTimestamp, rowIndex, sortConfig, span, startTimestampMs, style, }: Props, ref ) => { // Rows include the sortable header, the dataIndex does not const dataIndex = rowIndex - 1; const {getParamValue} = useUrlParams('n_detail_row', ''); const isDetailsOpen = getParamValue() === String(dataIndex); const startMs = span.startTimestamp * 1000; const endMs = span.endTimestamp * 1000; const method = span.data.method; const statusCode = span.data.statusCode; // `data.responseBodySize` is from SDK version 7.44-7.45 const size = span.data.size ?? span.data.response?.size ?? span.data.responseBodySize; const spanTime = useMemo( () => relativeTimeInMs(span.startTimestamp * 1000, startTimestampMs), [span.startTimestamp, startTimestampMs] ); const hasOccurred = currentTime >= spanTime; const isBeforeHover = currentHoverTime === undefined || currentHoverTime >= spanTime; const isByTimestamp = sortConfig.by === 'startTimestamp'; const isAsc = isByTimestamp ? sortConfig.asc : undefined; const columnProps = { className: classNames({ beforeCurrentTime: isByTimestamp ? isAsc ? hasOccurred : !hasOccurred : undefined, afterCurrentTime: isByTimestamp ? isAsc ? !hasOccurred : hasOccurred : undefined, beforeHoverTime: isByTimestamp && currentHoverTime !== undefined ? isAsc ? isBeforeHover : !isBeforeHover : undefined, afterHoverTime: isByTimestamp && currentHoverTime !== undefined ? isAsc ? !isBeforeHover : isBeforeHover : undefined, }), hasOccurred: isByTimestamp ? hasOccurred : undefined, isDetailsOpen, isStatusError: typeof statusCode === 'number' && statusCode >= 400, onClick: () => onClickCell({dataIndex, rowIndex}), onMouseEnter: () => handleMouseEnter(span), onMouseLeave: () => handleMouseLeave(span), ref, style, } as CellProps; const renderFns = [ () => ( {method ? method : 'GET'} ), () => ( {typeof statusCode === 'number' ? statusCode : EMPTY_CELL} ), () => ( {span.description || EMPTY_CELL} ), () => ( {operationName(span.op)} ), () => ( {size === undefined ? EMPTY_CELL : } ), () => ( {`${(endMs - startMs).toFixed(2)}ms`} ), () => ( { event.stopPropagation(); onClickTimestamp(span); }} startTimestampMs={startTimestampMs} timestampMs={startMs} /> ), ]; return renderFns[columnIndex](); } ); const cellBackground = p => { if (p.isDetailsOpen) { return `background-color: ${p.theme.textColor};`; } if (p.hasOccurred === undefined && !p.isStatusError) { const color = p.isHovered ? p.theme.hover : 'inherit'; return `background-color: ${color};`; } const color = p.isStatusError ? p.theme.alert.error.backgroundLight : 'inherit'; return `background-color: ${color};`; }; const cellColor = p => { if (p.isDetailsOpen) { const colors = p.isStatusError ? [p.theme.alert.error.background] : [p.theme.background]; return `color: ${colors[0]};`; } const colors = p.isStatusError ? [p.theme.alert.error.borderHover, p.theme.alert.error.iconColor] : ['inherit', p.theme.gray300]; return `color: ${p.hasOccurred !== false ? colors[0] : colors[1]};`; }; const Cell = styled('div')` display: flex; align-items: center; font-size: ${p => p.theme.fontSizeSmall}; cursor: ${p => (p.onClick ? 'pointer' : 'inherit')}; ${cellBackground} ${cellColor} ${p => p.numeric && ` font-variant-numeric: tabular-nums; justify-content: flex-end; `}; `; const Text = styled('div')` text-overflow: ellipsis; white-space: nowrap; overflow: hidden; padding: ${space(0.75)} ${space(1.5)}; `; const StyledTimestampButton = styled(TimestampButton)` padding-inline: ${space(1.5)}; `; export default NetworkTableCell;