consoleLogRow.tsx 4.4 KB

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