index.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import {useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import EmptyMessage from 'sentry/components/emptyMessage';
  4. import CompactSelect from 'sentry/components/forms/compactSelect';
  5. import {Panel} from 'sentry/components/panels';
  6. import {useReplayContext} from 'sentry/components/replays/replayContext';
  7. import {relativeTimeInMs} from 'sentry/components/replays/utils';
  8. import SearchBar from 'sentry/components/searchBar';
  9. import {t} from 'sentry/locale';
  10. import space from 'sentry/styles/space';
  11. import type {BreadcrumbTypeDefault, Crumb} from 'sentry/types/breadcrumbs';
  12. import {defined} from 'sentry/utils';
  13. import {getPrevReplayEvent} from 'sentry/utils/replays/getReplayEvent';
  14. import {useCurrentItemScroller} from 'sentry/utils/replays/hooks/useCurrentItemScroller';
  15. import ConsoleMessage from 'sentry/views/replays/detail/console/consoleMessage';
  16. import useConsoleFilters from 'sentry/views/replays/detail/console/useConsoleFilters';
  17. import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
  18. interface Props {
  19. breadcrumbs: Extract<Crumb, BreadcrumbTypeDefault>[];
  20. startTimestampMs: number;
  21. }
  22. function Console({breadcrumbs, startTimestampMs = 0}: Props) {
  23. const {currentHoverTime, currentTime} = useReplayContext();
  24. const containerRef = useRef<HTMLDivElement>(null);
  25. useCurrentItemScroller(containerRef);
  26. const {items, logLevel, searchTerm, getOptions, setLogLevel, setSearchTerm} =
  27. useConsoleFilters({
  28. breadcrumbs,
  29. });
  30. const currentUserAction = getPrevReplayEvent({
  31. items: breadcrumbs,
  32. targetTimestampMs: startTimestampMs + currentTime,
  33. allowExact: true,
  34. allowEqual: true,
  35. });
  36. const closestUserAction =
  37. currentHoverTime !== undefined
  38. ? getPrevReplayEvent({
  39. items: breadcrumbs,
  40. targetTimestampMs: startTimestampMs + (currentHoverTime ?? 0),
  41. allowExact: true,
  42. allowEqual: true,
  43. })
  44. : undefined;
  45. const isOcurring = (breadcrumb: Crumb, closestBreadcrumb?: Crumb): boolean => {
  46. if (!defined(currentHoverTime) || !defined(closestBreadcrumb)) {
  47. return false;
  48. }
  49. const isCurrentBreadcrumb = closestBreadcrumb.id === breadcrumb.id;
  50. // We don't want to hightlight the breadcrumb if it's more than 1 second away from the current hover time
  51. const isMoreThanASecondOfDiff =
  52. Math.trunc(currentHoverTime / 1000) >
  53. Math.trunc(
  54. relativeTimeInMs(closestBreadcrumb.timestamp || '', startTimestampMs) / 1000
  55. );
  56. return isCurrentBreadcrumb && !isMoreThanASecondOfDiff;
  57. };
  58. return (
  59. <ConsoleContainer>
  60. <ConsoleFilters>
  61. <CompactSelect
  62. triggerProps={{prefix: t('Log Level')}}
  63. triggerLabel={logLevel.length === 0 ? t('Any') : null}
  64. multiple
  65. options={getOptions()}
  66. onChange={selected => setLogLevel(selected.map(_ => _.value))}
  67. size="sm"
  68. value={logLevel}
  69. />
  70. <SearchBar
  71. onChange={setSearchTerm}
  72. placeholder={t('Search console logs...')}
  73. size="sm"
  74. query={searchTerm}
  75. />
  76. </ConsoleFilters>
  77. <ConsoleMessageContainer ref={containerRef}>
  78. {items.length > 0 ? (
  79. <ConsoleTable>
  80. {items.map((breadcrumb, i) => {
  81. return (
  82. <ConsoleMessage
  83. isActive={closestUserAction?.id === breadcrumb.id}
  84. isCurrent={currentUserAction?.id === breadcrumb.id}
  85. isOcurring={isOcurring(breadcrumb, closestUserAction)}
  86. startTimestampMs={startTimestampMs}
  87. key={breadcrumb.id}
  88. isLast={i === breadcrumbs.length - 1}
  89. breadcrumb={breadcrumb}
  90. hasOccurred={
  91. currentTime >=
  92. relativeTimeInMs(breadcrumb?.timestamp || '', startTimestampMs)
  93. }
  94. />
  95. );
  96. })}
  97. </ConsoleTable>
  98. ) : (
  99. <StyledEmptyMessage title={t('No results found.')} />
  100. )}
  101. </ConsoleMessageContainer>
  102. </ConsoleContainer>
  103. );
  104. }
  105. const ConsoleContainer = styled(FluidHeight)`
  106. height: 100%;
  107. `;
  108. const ConsoleFilters = styled('div')`
  109. display: grid;
  110. gap: ${space(1)};
  111. grid-template-columns: max-content 1fr;
  112. margin-bottom: ${space(1)};
  113. @media (max-width: ${p => p.theme.breakpoints.small}) {
  114. margin-top: ${space(1)};
  115. }
  116. `;
  117. const ConsoleMessageContainer = styled(FluidHeight)`
  118. overflow: auto;
  119. border-radius: ${p => p.theme.borderRadius};
  120. border: 1px solid ${p => p.theme.border};
  121. box-shadow: ${p => p.theme.dropShadowLight};
  122. `;
  123. const StyledEmptyMessage = styled(EmptyMessage)`
  124. align-items: center;
  125. `;
  126. const ConsoleTable = styled(Panel)`
  127. display: grid;
  128. grid-template-columns: max-content auto max-content;
  129. width: 100%;
  130. font-family: ${p => p.theme.text.familyMono};
  131. font-size: 0.8em;
  132. border: none;
  133. box-shadow: none;
  134. margin-bottom: 0;
  135. `;
  136. export default Console;