consoleLogRow.tsx 4.4 KB

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