splitCrumbs.tsx 3.6 KB

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