breadcrumbItem.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import {memo, useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import BreadcrumbIcon from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type/icon';
  4. import {PanelItem} from 'sentry/components/panels';
  5. import {getDetails} from 'sentry/components/replays/breadcrumbs/utils';
  6. import PlayerRelativeTime from 'sentry/components/replays/playerRelativeTime';
  7. import {SVGIconProps} from 'sentry/icons/svgIcon';
  8. import space from 'sentry/styles/space';
  9. import type {Crumb} from 'sentry/types/breadcrumbs';
  10. import useActiveReplayTab from 'sentry/utils/replays/hooks/useActiveReplayTab';
  11. type MouseCallback = (crumb: Crumb, e: React.MouseEvent<HTMLElement>) => void;
  12. interface Props {
  13. crumb: Crumb;
  14. isHovered: boolean;
  15. isSelected: boolean;
  16. onClick: null | MouseCallback;
  17. startTimestampMs: number;
  18. allowHover?: boolean;
  19. onMouseEnter?: MouseCallback;
  20. onMouseLeave?: MouseCallback;
  21. }
  22. function BreadcrumbItem({
  23. crumb,
  24. isHovered,
  25. isSelected,
  26. startTimestampMs,
  27. allowHover = true,
  28. onMouseEnter,
  29. onMouseLeave,
  30. onClick,
  31. }: Props) {
  32. const {title, description} = getDetails(crumb, startTimestampMs);
  33. const {setActiveTab} = useActiveReplayTab();
  34. const handleMouseEnter = useCallback(
  35. (e: React.MouseEvent<HTMLElement>) => onMouseEnter && onMouseEnter(crumb, e),
  36. [onMouseEnter, crumb]
  37. );
  38. const handleMouseLeave = useCallback(
  39. (e: React.MouseEvent<HTMLElement>) => onMouseLeave && onMouseLeave(crumb, e),
  40. [onMouseLeave, crumb]
  41. );
  42. const handleClick = useCallback(
  43. (e: React.MouseEvent<HTMLElement>) => {
  44. onClick?.(crumb, e);
  45. switch (crumb.type) {
  46. case 'navigation':
  47. case 'debug':
  48. setActiveTab('network');
  49. break;
  50. case 'ui':
  51. setActiveTab('dom');
  52. break;
  53. case 'error':
  54. default:
  55. setActiveTab('console');
  56. break;
  57. }
  58. },
  59. [crumb, setActiveTab, onClick]
  60. );
  61. return (
  62. <CrumbItem
  63. as={onClick ? 'button' : 'span'}
  64. onMouseEnter={handleMouseEnter}
  65. onMouseLeave={handleMouseLeave}
  66. onClick={handleClick}
  67. isHovered={isHovered}
  68. isSelected={isSelected}
  69. aria-current={isSelected}
  70. allowHover={allowHover}
  71. >
  72. <IconWrapper color={crumb.color}>
  73. <BreadcrumbIcon type={crumb.type} />
  74. </IconWrapper>
  75. <CrumbDetails>
  76. <TitleContainer>
  77. <Title>{title}</Title>
  78. <PlayerRelativeTime
  79. relativeTimeMs={startTimestampMs}
  80. timestamp={crumb.timestamp}
  81. />
  82. </TitleContainer>
  83. <Description title={description}>{description}</Description>
  84. </CrumbDetails>
  85. </CrumbItem>
  86. );
  87. }
  88. const CrumbDetails = styled('div')`
  89. display: flex;
  90. flex-direction: column;
  91. overflow: hidden;
  92. `;
  93. const TitleContainer = styled('div')`
  94. display: flex;
  95. justify-content: space-between;
  96. gap: ${space(1)};
  97. `;
  98. const Title = styled('span')`
  99. ${p => p.theme.overflowEllipsis};
  100. text-transform: capitalize;
  101. font-weight: 600;
  102. color: ${p => p.theme.gray400};
  103. line-height: ${p => p.theme.text.lineHeightBody};
  104. `;
  105. const Description = styled('span')`
  106. ${p => p.theme.overflowEllipsis};
  107. font-size: 0.7rem;
  108. font-variant-numeric: tabular-nums;
  109. line-height: ${p => p.theme.text.lineHeightBody};
  110. color: ${p => p.theme.subText};
  111. `;
  112. type CrumbItemProps = {
  113. isHovered: boolean;
  114. isSelected: boolean;
  115. allowHover?: boolean;
  116. };
  117. const CrumbItem = styled(PanelItem)<CrumbItemProps>`
  118. display: grid;
  119. grid-template-columns: max-content auto;
  120. align-items: flex-start;
  121. gap: ${space(1)};
  122. width: 100%;
  123. font-size: ${p => p.theme.fontSizeMedium};
  124. background: transparent;
  125. padding: ${space(1)};
  126. text-align: left;
  127. border: none;
  128. position: relative;
  129. ${p => p.isSelected && `background-color: ${p.theme.purple100};`}
  130. ${p => p.isHovered && `background-color: ${p.theme.surface100};`}
  131. border-radius: ${p => p.theme.borderRadius};
  132. ${p =>
  133. p.allowHover &&
  134. ` &:hover {
  135. background-color: ${p.theme.surface100};
  136. }`}
  137. /* Draw a vertical line behind the breadcrumb icon. The line connects each row together, but is truncated for the first and last items */
  138. &::after {
  139. content: '';
  140. position: absolute;
  141. left: 19.5px;
  142. width: 1px;
  143. background: ${p => p.theme.gray200};
  144. height: 100%;
  145. }
  146. &:first-of-type::after {
  147. top: ${space(1)};
  148. bottom: 0;
  149. }
  150. &:last-of-type::after {
  151. top: 0;
  152. height: ${space(1)};
  153. }
  154. &:only-of-type::after {
  155. height: 0;
  156. }
  157. `;
  158. /**
  159. * Taken `from events/interfaces/.../breadcrumbs/types`
  160. */
  161. const IconWrapper = styled('div')<Required<Pick<SVGIconProps, 'color'>>>`
  162. display: flex;
  163. align-items: center;
  164. justify-content: center;
  165. width: 24px;
  166. height: 24px;
  167. border-radius: 50%;
  168. color: ${p => p.theme.white};
  169. background: ${p => p.theme[p.color] ?? p.color};
  170. box-shadow: ${p => p.theme.dropShadowLightest};
  171. position: relative;
  172. z-index: ${p => p.theme.zIndex.initial};
  173. `;
  174. const MemoizedBreadcrumbItem = memo(BreadcrumbItem);
  175. export default MemoizedBreadcrumbItem;