consoleLogRow.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import {CSSProperties, memo, useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import classNames from 'classnames';
  4. import ErrorBoundary from 'sentry/components/errorBoundary';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {IconClose, IconInfo, IconWarning} from 'sentry/icons';
  7. import {space} from 'sentry/styles/space';
  8. import {BreadcrumbLevelType} from 'sentry/types/breadcrumbs';
  9. import type useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
  10. import type {BreadcrumbFrame, ConsoleFrame} from 'sentry/utils/replays/types';
  11. import MessageFormatter from 'sentry/views/replays/detail/console/messageFormatter';
  12. import TimestampButton from 'sentry/views/replays/detail/timestampButton';
  13. import {OnDimensionChange} from 'sentry/views/replays/detail/useVirtualizedInspector';
  14. interface Props extends ReturnType<typeof useCrumbHandlers> {
  15. currentHoverTime: number | undefined;
  16. currentTime: number;
  17. frame: BreadcrumbFrame;
  18. index: number;
  19. startTimestampMs: number;
  20. style: CSSProperties;
  21. expandPaths?: string[];
  22. onDimensionChange?: OnDimensionChange;
  23. }
  24. function UnmemoizedConsoleLogRow({
  25. currentHoverTime,
  26. currentTime,
  27. expandPaths,
  28. frame,
  29. onMouseEnter,
  30. onMouseLeave,
  31. index,
  32. onClickTimestamp,
  33. onDimensionChange,
  34. startTimestampMs,
  35. style,
  36. }: Props) {
  37. const handleDimensionChange = useCallback(
  38. (path, expandedState, e) =>
  39. onDimensionChange && onDimensionChange(index, path, expandedState, e),
  40. [onDimensionChange, index]
  41. );
  42. const hasOccurred = currentTime >= frame.offsetMs;
  43. const isBeforeHover =
  44. currentHoverTime === undefined || currentHoverTime >= frame.offsetMs;
  45. return (
  46. <ConsoleLog
  47. className={classNames({
  48. beforeCurrentTime: hasOccurred,
  49. afterCurrentTime: !hasOccurred,
  50. beforeHoverTime: currentHoverTime !== undefined && isBeforeHover,
  51. afterHoverTime: currentHoverTime !== undefined && !isBeforeHover,
  52. })}
  53. hasOccurred={hasOccurred}
  54. level={(frame as ConsoleFrame).level}
  55. onMouseEnter={() => onMouseEnter(frame)}
  56. onMouseLeave={() => onMouseLeave(frame)}
  57. style={style}
  58. >
  59. <ConsoleLevelIcon level={(frame as ConsoleFrame).level} />
  60. <Message>
  61. <ErrorBoundary mini>
  62. <MessageFormatter
  63. expandPaths={expandPaths}
  64. frame={frame}
  65. onExpand={handleDimensionChange}
  66. />
  67. </ErrorBoundary>
  68. </Message>
  69. <TimestampButton
  70. onClick={event => {
  71. event.stopPropagation();
  72. onClickTimestamp(frame);
  73. }}
  74. startTimestampMs={startTimestampMs}
  75. timestampMs={frame.timestampMs}
  76. />
  77. </ConsoleLog>
  78. );
  79. }
  80. const ConsoleLog = styled('div')<{
  81. hasOccurred: boolean;
  82. level: undefined | string;
  83. }>`
  84. display: grid;
  85. grid-template-columns: 12px 1fr max-content;
  86. gap: ${space(0.75)};
  87. align-items: baseline;
  88. padding: ${space(0.5)} ${space(1)};
  89. font-size: ${p => p.theme.fontSizeSmall};
  90. background-color: ${p =>
  91. ['warning', 'error'].includes(String(p.level))
  92. ? p.theme.alert[String(p.level)].backgroundLight
  93. : 'inherit'};
  94. /* Overridden in TabItemContainer, depending on *CurrentTime and *HoverTime classes */
  95. border-top: 1px solid transparent;
  96. border-bottom: 1px solid transparent;
  97. color: ${p =>
  98. ['warning', 'error'].includes(String(p.level))
  99. ? p.theme.alert[String(p.level)].iconColor
  100. : p.hasOccurred
  101. ? 'inherit'
  102. : p.theme.gray300};
  103. /*
  104. Show the timestamp button "Play" icon when we hover the row.
  105. This is a really generic selector that could find many things, but for now it
  106. only targets the one thing that we expect.
  107. */
  108. &:hover button > svg {
  109. visibility: visible;
  110. }
  111. `;
  112. const ICONS = {
  113. [BreadcrumbLevelType.ERROR]: (
  114. <Tooltip title={BreadcrumbLevelType.ERROR}>
  115. <IconClose size="xs" isCircled />
  116. </Tooltip>
  117. ),
  118. [BreadcrumbLevelType.WARNING]: (
  119. <Tooltip title={BreadcrumbLevelType.WARNING}>
  120. <IconWarning size="xs" />
  121. </Tooltip>
  122. ),
  123. [BreadcrumbLevelType.INFO]: (
  124. <Tooltip title={BreadcrumbLevelType.INFO}>
  125. <IconInfo size="xs" />
  126. </Tooltip>
  127. ),
  128. };
  129. const MediumFontSize = styled('span')`
  130. font-size: ${p => p.theme.fontSizeMedium};
  131. `;
  132. function ConsoleLevelIcon({level}: {level: string | undefined}) {
  133. return level && level in ICONS ? (
  134. <MediumFontSize>{ICONS[level]}</MediumFontSize>
  135. ) : (
  136. <i />
  137. );
  138. }
  139. const Message = styled('div')`
  140. font-family: ${p => p.theme.text.familyMono};
  141. white-space: pre-wrap;
  142. word-break: break-word;
  143. `;
  144. const ConsoleLogRow = memo(UnmemoizedConsoleLogRow);
  145. export default ConsoleLogRow;