splitCrumbs.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import styled from '@emotion/styled';
  2. import {Hovercard} from 'sentry/components/hovercard';
  3. import BreadcrumbItem from 'sentry/components/replays/breadcrumbs/breadcrumbItem';
  4. import TextOverflow from 'sentry/components/textOverflow';
  5. import {tn} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import {Crumb} from 'sentry/types/breadcrumbs';
  8. import {getDescription} from 'sentry/utils/replays/frame';
  9. import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
  10. import type {ReplayFrame} from 'sentry/utils/replays/types';
  11. type MaybeOnClickHandler = null | ((frame: Crumb | ReplayFrame) => void);
  12. function splitCrumbs({
  13. frames,
  14. onClick,
  15. startTimestampMs,
  16. }: {
  17. frames: ReplayFrame[];
  18. onClick: MaybeOnClickHandler;
  19. startTimestampMs: number;
  20. }) {
  21. const firstFrame = frames.slice(0, 1);
  22. const summarizedFrames = frames.slice(1, -1);
  23. const lastFrame = frames.slice(-1);
  24. if (frames.length === 0) {
  25. // This one shouldn't overflow, but by including <TextOverflow> css stays
  26. // consistent with the other Segment types
  27. return [
  28. <Span key="summary">
  29. <TextOverflow>{tn('%s Page', '%s Pages', 0)}</TextOverflow>
  30. </Span>,
  31. ];
  32. }
  33. if (frames.length > 3) {
  34. return [
  35. <SummarySegment
  36. key="first"
  37. frames={firstFrame}
  38. startTimestampMs={startTimestampMs}
  39. handleOnClick={onClick}
  40. />,
  41. <SummarySegment
  42. key="summary"
  43. frames={summarizedFrames}
  44. startTimestampMs={startTimestampMs}
  45. handleOnClick={onClick}
  46. />,
  47. <SummarySegment
  48. key="last"
  49. frames={lastFrame}
  50. startTimestampMs={startTimestampMs}
  51. handleOnClick={onClick}
  52. />,
  53. ];
  54. }
  55. return frames.map((frame, i) => (
  56. <SummarySegment
  57. key={i}
  58. frames={[frame]}
  59. startTimestampMs={startTimestampMs}
  60. handleOnClick={onClick}
  61. />
  62. ));
  63. }
  64. function SummarySegment({
  65. frames,
  66. handleOnClick,
  67. startTimestampMs,
  68. }: {
  69. frames: ReplayFrame[];
  70. handleOnClick: MaybeOnClickHandler;
  71. startTimestampMs: number;
  72. }) {
  73. const {handleMouseEnter, handleMouseLeave} = useCrumbHandlers(startTimestampMs);
  74. const summaryItems = (
  75. <ScrollingList>
  76. {frames.map((frame, i) => (
  77. <li key={i}>
  78. <BreadcrumbItem
  79. crumb={frame}
  80. onClick={handleOnClick}
  81. onMouseEnter={handleMouseEnter}
  82. onMouseLeave={handleMouseLeave}
  83. startTimestampMs={startTimestampMs}
  84. />
  85. </li>
  86. ))}
  87. </ScrollingList>
  88. );
  89. const label =
  90. frames.length === 1
  91. ? getDescription(frames[0])
  92. : tn('%s Page', '%s Pages', frames.length);
  93. return (
  94. <Span>
  95. <HalfPaddingHovercard body={summaryItems} position="right">
  96. <TextOverflow>{label}</TextOverflow>
  97. </HalfPaddingHovercard>
  98. </Span>
  99. );
  100. }
  101. const ScrollingList = styled('ul')`
  102. padding: 0;
  103. margin: 0;
  104. list-style: none;
  105. max-height: calc(100vh - 32px);
  106. overflow: scroll;
  107. `;
  108. const Span = styled('span')`
  109. color: ${p => p.theme.gray500};
  110. font-size: ${p => p.theme.fontSizeMedium};
  111. line-height: 0;
  112. `;
  113. const HalfPaddingHovercard = styled(
  114. ({children, bodyClassName, ...props}: React.ComponentProps<typeof Hovercard>) => (
  115. <Hovercard bodyClassName={bodyClassName || '' + ' half-padding'} {...props}>
  116. {children}
  117. </Hovercard>
  118. )
  119. )`
  120. .half-padding {
  121. padding: ${space(0.5)};
  122. }
  123. `;
  124. export default splitCrumbs;