index.tsx 3.7 KB

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