index.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import {Fragment, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  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, showPlayerTime} 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 {BreadcrumbLevelType, BreadcrumbTypeDefault} from 'sentry/types/breadcrumbs';
  12. import EmptyMessage from 'sentry/views/settings/components/emptyMessage';
  13. import ConsoleMessage from './consoleMessage';
  14. import {filterBreadcrumbs} from './utils';
  15. interface Props {
  16. breadcrumbs: BreadcrumbTypeDefault[];
  17. startTimestamp: number;
  18. }
  19. const getDistinctLogLevels = (breadcrumbs: BreadcrumbTypeDefault[]) =>
  20. Array.from(new Set<string>(breadcrumbs.map(breadcrumb => breadcrumb.level)));
  21. function Console({breadcrumbs, startTimestamp = 0}: Props) {
  22. const {currentHoverTime, currentTime} = useReplayContext();
  23. const [searchTerm, setSearchTerm] = useState('');
  24. const [logLevel, setLogLevel] = useState<BreadcrumbLevelType[]>([]);
  25. const handleSearch = debounce(query => setSearchTerm(query), 150);
  26. const filteredBreadcrumbs = useMemo(
  27. () => filterBreadcrumbs(breadcrumbs, searchTerm, logLevel),
  28. [logLevel, searchTerm, breadcrumbs]
  29. );
  30. const activeConsoleBounds = useMemo(() => {
  31. if (filteredBreadcrumbs.length <= 0 || currentHoverTime === undefined) {
  32. return [-1, -1];
  33. }
  34. let indexUpperBound = 0;
  35. const finalBreadCrumbIndex = filteredBreadcrumbs.length - 1;
  36. const finalBreadcrumbTimestamp =
  37. filteredBreadcrumbs[finalBreadCrumbIndex].timestamp || '';
  38. if (currentHoverTime >= relativeTimeInMs(finalBreadcrumbTimestamp, startTimestamp)) {
  39. indexUpperBound = finalBreadCrumbIndex;
  40. } else {
  41. indexUpperBound =
  42. filteredBreadcrumbs.findIndex(
  43. breadcrumb =>
  44. relativeTimeInMs(breadcrumb.timestamp || '', startTimestamp) >=
  45. (currentHoverTime || 0)
  46. ) - 1;
  47. }
  48. const activeMessageBoundary = showPlayerTime(
  49. filteredBreadcrumbs[indexUpperBound]?.timestamp || '',
  50. startTimestamp
  51. );
  52. const indexLowerBound = filteredBreadcrumbs.findIndex(
  53. breadcrumb =>
  54. showPlayerTime(breadcrumb.timestamp || '', startTimestamp) ===
  55. activeMessageBoundary
  56. );
  57. return [indexLowerBound, indexUpperBound];
  58. }, [currentHoverTime, filteredBreadcrumbs, startTimestamp]);
  59. return (
  60. <Fragment>
  61. <ConsoleFilters>
  62. <CompactSelect
  63. triggerProps={{
  64. prefix: t('Log Level'),
  65. }}
  66. multiple
  67. options={getDistinctLogLevels(breadcrumbs).map(breadcrumbLogLevel => ({
  68. value: breadcrumbLogLevel,
  69. label: breadcrumbLogLevel,
  70. }))}
  71. onChange={selections =>
  72. setLogLevel(selections.map(selection => selection.value))
  73. }
  74. />
  75. <SearchBar onChange={handleSearch} placeholder={t('Search console logs...')} />
  76. </ConsoleFilters>
  77. {filteredBreadcrumbs.length > 0 ? (
  78. <ConsoleTable>
  79. {filteredBreadcrumbs.map((breadcrumb, i) => {
  80. return (
  81. <ConsoleMessage
  82. isActive={i >= activeConsoleBounds[0] && i <= activeConsoleBounds[1]}
  83. startTimestamp={startTimestamp}
  84. key={i}
  85. isLast={i === breadcrumbs.length - 1}
  86. breadcrumb={breadcrumb}
  87. hasOccurred={
  88. currentTime >=
  89. relativeTimeInMs(breadcrumb?.timestamp || '', startTimestamp)
  90. }
  91. />
  92. );
  93. })}
  94. </ConsoleTable>
  95. ) : (
  96. <StyledEmptyMessage title={t('No results found.')} />
  97. )}
  98. </Fragment>
  99. );
  100. }
  101. const ConsoleFilters = styled('div')`
  102. display: grid;
  103. gap: ${space(1)};
  104. grid-template-columns: max-content 1fr;
  105. margin-bottom: ${space(1)};
  106. @media (max-width: ${p => p.theme.breakpoints.small}) {
  107. margin-top: ${space(1)};
  108. }
  109. `;
  110. const StyledEmptyMessage = styled(EmptyMessage)`
  111. align-items: center;
  112. `;
  113. const ConsoleTable = styled(Panel)`
  114. display: grid;
  115. grid-template-columns: max-content auto max-content;
  116. width: 100%;
  117. font-family: ${p => p.theme.text.familyMono};
  118. font-size: 0.8em;
  119. `;
  120. export default Console;