networkTableCell.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import {CSSProperties, forwardRef, MouseEvent, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import classNames from 'classnames';
  4. import FileSize from 'sentry/components/fileSize';
  5. import {relativeTimeInMs} from 'sentry/components/replays/utils';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {space} from 'sentry/styles/space';
  8. import useUrlParams from 'sentry/utils/useUrlParams';
  9. import useSortNetwork from 'sentry/views/replays/detail/network/useSortNetwork';
  10. import TimestampButton from 'sentry/views/replays/detail/timestampButton';
  11. import {operationName} from 'sentry/views/replays/detail/utils';
  12. import type {NetworkSpan} from 'sentry/views/replays/types';
  13. const EMPTY_CELL = '--';
  14. type Props = {
  15. columnIndex: number;
  16. currentHoverTime: number | undefined;
  17. currentTime: number;
  18. handleMouseEnter: (span: NetworkSpan) => void;
  19. handleMouseLeave: (span: NetworkSpan) => void;
  20. onClickCell: (props: {dataIndex: number; rowIndex: number}) => void;
  21. onClickTimestamp: (crumb: NetworkSpan) => void;
  22. rowIndex: number;
  23. sortConfig: ReturnType<typeof useSortNetwork>['sortConfig'];
  24. span: NetworkSpan;
  25. startTimestampMs: number;
  26. style: CSSProperties;
  27. };
  28. type CellProps = {
  29. hasOccurred: boolean | undefined;
  30. isDetailsOpen: boolean;
  31. isStatusError: boolean;
  32. className?: string;
  33. numeric?: boolean;
  34. onClick?: undefined | (() => void);
  35. };
  36. const NetworkTableCell = forwardRef<HTMLDivElement, Props>(
  37. (
  38. {
  39. columnIndex,
  40. currentHoverTime,
  41. currentTime,
  42. handleMouseEnter,
  43. handleMouseLeave,
  44. onClickCell,
  45. onClickTimestamp,
  46. rowIndex,
  47. sortConfig,
  48. span,
  49. startTimestampMs,
  50. style,
  51. }: Props,
  52. ref
  53. ) => {
  54. // Rows include the sortable header, the dataIndex does not
  55. const dataIndex = rowIndex - 1;
  56. const {getParamValue} = useUrlParams('n_detail_row', '');
  57. const isDetailsOpen = getParamValue() === String(dataIndex);
  58. const startMs = span.startTimestamp * 1000;
  59. const endMs = span.endTimestamp * 1000;
  60. const method = span.data.method;
  61. const statusCode = span.data.statusCode;
  62. // `data.responseBodySize` is from SDK version 7.44-7.45
  63. const size = span.data.size ?? span.data.response?.size ?? span.data.responseBodySize;
  64. const spanTime = useMemo(
  65. () => relativeTimeInMs(span.startTimestamp * 1000, startTimestampMs),
  66. [span.startTimestamp, startTimestampMs]
  67. );
  68. const hasOccurred = currentTime >= spanTime;
  69. const isBeforeHover = currentHoverTime === undefined || currentHoverTime >= spanTime;
  70. const isByTimestamp = sortConfig.by === 'startTimestamp';
  71. const isAsc = isByTimestamp ? sortConfig.asc : undefined;
  72. const columnProps = {
  73. className: classNames({
  74. beforeCurrentTime: isByTimestamp
  75. ? isAsc
  76. ? hasOccurred
  77. : !hasOccurred
  78. : undefined,
  79. afterCurrentTime: isByTimestamp
  80. ? isAsc
  81. ? !hasOccurred
  82. : hasOccurred
  83. : undefined,
  84. beforeHoverTime:
  85. isByTimestamp && currentHoverTime !== undefined
  86. ? isAsc
  87. ? isBeforeHover
  88. : !isBeforeHover
  89. : undefined,
  90. afterHoverTime:
  91. isByTimestamp && currentHoverTime !== undefined
  92. ? isAsc
  93. ? !isBeforeHover
  94. : isBeforeHover
  95. : undefined,
  96. }),
  97. hasOccurred: isByTimestamp ? hasOccurred : undefined,
  98. isDetailsOpen,
  99. isStatusError: typeof statusCode === 'number' && statusCode >= 400,
  100. onClick: () => onClickCell({dataIndex, rowIndex}),
  101. onMouseEnter: () => handleMouseEnter(span),
  102. onMouseLeave: () => handleMouseLeave(span),
  103. ref,
  104. style,
  105. } as CellProps;
  106. const renderFns = [
  107. () => (
  108. <Cell {...columnProps}>
  109. <Text>{method ? method : 'GET'}</Text>
  110. </Cell>
  111. ),
  112. () => (
  113. <Cell {...columnProps}>
  114. <Text>{typeof statusCode === 'number' ? statusCode : EMPTY_CELL}</Text>
  115. </Cell>
  116. ),
  117. () => (
  118. <Cell {...columnProps}>
  119. <Tooltip
  120. title={span.description}
  121. isHoverable
  122. showOnlyOnOverflow
  123. overlayStyle={{maxWidth: '500px !important'}}
  124. >
  125. <Text>{span.description || EMPTY_CELL}</Text>
  126. </Tooltip>
  127. </Cell>
  128. ),
  129. () => (
  130. <Cell {...columnProps}>
  131. <Tooltip title={operationName(span.op)} isHoverable showOnlyOnOverflow>
  132. <Text>{operationName(span.op)}</Text>
  133. </Tooltip>
  134. </Cell>
  135. ),
  136. () => (
  137. <Cell {...columnProps} numeric>
  138. <Text>
  139. {size === undefined ? EMPTY_CELL : <FileSize base={10} bytes={size} />}
  140. </Text>
  141. </Cell>
  142. ),
  143. () => (
  144. <Cell {...columnProps} numeric>
  145. <Text>{`${(endMs - startMs).toFixed(2)}ms`}</Text>
  146. </Cell>
  147. ),
  148. () => (
  149. <Cell {...columnProps} numeric>
  150. <StyledTimestampButton
  151. format="mm:ss.SSS"
  152. onClick={(event: MouseEvent) => {
  153. event.stopPropagation();
  154. onClickTimestamp(span);
  155. }}
  156. startTimestampMs={startTimestampMs}
  157. timestampMs={startMs}
  158. />
  159. </Cell>
  160. ),
  161. ];
  162. return renderFns[columnIndex]();
  163. }
  164. );
  165. const cellBackground = p => {
  166. if (p.isDetailsOpen) {
  167. return `background-color: ${p.theme.textColor};`;
  168. }
  169. if (p.hasOccurred === undefined && !p.isStatusError) {
  170. const color = p.isHovered ? p.theme.hover : 'inherit';
  171. return `background-color: ${color};`;
  172. }
  173. const color = p.isStatusError ? p.theme.alert.error.backgroundLight : 'inherit';
  174. return `background-color: ${color};`;
  175. };
  176. const cellColor = p => {
  177. if (p.isDetailsOpen) {
  178. const colors = p.isStatusError
  179. ? [p.theme.alert.error.background]
  180. : [p.theme.background];
  181. return `color: ${colors[0]};`;
  182. }
  183. const colors = p.isStatusError
  184. ? [p.theme.alert.error.borderHover, p.theme.alert.error.iconColor]
  185. : ['inherit', p.theme.gray300];
  186. return `color: ${p.hasOccurred !== false ? colors[0] : colors[1]};`;
  187. };
  188. const Cell = styled('div')<CellProps>`
  189. display: flex;
  190. align-items: center;
  191. font-size: ${p => p.theme.fontSizeSmall};
  192. cursor: ${p => (p.onClick ? 'pointer' : 'inherit')};
  193. ${cellBackground}
  194. ${cellColor}
  195. ${p =>
  196. p.numeric &&
  197. `
  198. font-variant-numeric: tabular-nums;
  199. justify-content: flex-end;
  200. `};
  201. `;
  202. const Text = styled('div')`
  203. text-overflow: ellipsis;
  204. white-space: nowrap;
  205. overflow: hidden;
  206. padding: ${space(0.75)} ${space(1.5)};
  207. `;
  208. const StyledTimestampButton = styled(TimestampButton)`
  209. padding-inline: ${space(1.5)};
  210. `;
  211. export default NetworkTableCell;