consoleLogRow.tsx 4.9 KB

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