breadcrumbItem.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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 SvgIcon from 'sentry/icons/svgIcon';
  8. import space from 'sentry/styles/space';
  9. import type {Crumb} from 'sentry/types/breadcrumbs';
  10. type MouseCallback = (crumb: Crumb, e: React.MouseEvent<HTMLElement>) => void;
  11. interface Props {
  12. crumb: Crumb;
  13. isHovered: boolean;
  14. isSelected: boolean;
  15. onClick: MouseCallback;
  16. onMouseEnter: MouseCallback;
  17. onMouseLeave: MouseCallback;
  18. startTimestamp: number;
  19. }
  20. function BreadcrumbItem({
  21. crumb,
  22. isHovered,
  23. isSelected,
  24. startTimestamp,
  25. onMouseEnter,
  26. onMouseLeave,
  27. onClick,
  28. }: Props) {
  29. const {title, description} = getDetails(crumb);
  30. const handleMouseEnter = useCallback(
  31. (e: React.MouseEvent<HTMLElement>) => onMouseEnter(crumb, e),
  32. [onMouseEnter, crumb]
  33. );
  34. const handleMouseLeave = useCallback(
  35. (e: React.MouseEvent<HTMLElement>) => onMouseLeave(crumb, e),
  36. [onMouseLeave, crumb]
  37. );
  38. const handleClick = useCallback(
  39. (e: React.MouseEvent<HTMLElement>) => onClick(crumb, e),
  40. [onClick, crumb]
  41. );
  42. return (
  43. <CrumbItem
  44. as="button"
  45. onMouseEnter={handleMouseEnter}
  46. onMouseLeave={handleMouseLeave}
  47. onClick={handleClick}
  48. isHovered={isHovered}
  49. isSelected={isSelected}
  50. >
  51. <IconWrapper color={crumb.color}>
  52. <BreadcrumbIcon type={crumb.type} />
  53. </IconWrapper>
  54. <CrumbDetails>
  55. <Title>{title}</Title>
  56. <Description title={description}>{description}</Description>
  57. </CrumbDetails>
  58. <PlayerRelativeTime relativeTime={startTimestamp} timestamp={crumb.timestamp} />
  59. </CrumbItem>
  60. );
  61. }
  62. const CrumbDetails = styled('div')`
  63. display: flex;
  64. flex-direction: column;
  65. overflow: hidden;
  66. line-height: 1.2;
  67. padding: ${space(1)} 0;
  68. `;
  69. const Title = styled('span')`
  70. ${p => p.theme.overflowEllipsis};
  71. text-transform: capitalize;
  72. `;
  73. const Description = styled('span')`
  74. ${p => p.theme.overflowEllipsis};
  75. font-size: 0.7rem;
  76. font-family: ${p => p.theme.text.familyMono};
  77. `;
  78. type CrumbItemProps = {
  79. isHovered: boolean;
  80. isSelected: boolean;
  81. };
  82. const CrumbItem = styled(PanelItem)<CrumbItemProps>`
  83. display: grid;
  84. grid-template-columns: max-content max-content auto max-content;
  85. align-items: center;
  86. gap: ${space(1)};
  87. width: 100%;
  88. font-size: ${p => p.theme.fontSizeMedium};
  89. background: transparent;
  90. padding: 0;
  91. padding-right: ${space(1)};
  92. text-align: left;
  93. border: none;
  94. border-bottom: 1px solid ${p => p.theme.innerBorder};
  95. ${p => p.isHovered && `background: ${p.theme.surface400};`}
  96. /* overrides PanelItem css */
  97. &:last-child {
  98. border-bottom: 1px solid ${p => p.theme.innerBorder};
  99. }
  100. /* Selected state */
  101. ::before {
  102. content: '';
  103. width: 4px;
  104. height: 100%;
  105. ${p => p.isSelected && `background-color: ${p.theme.purple300};`}
  106. }
  107. `;
  108. /**
  109. * Taken `from events/interfaces/.../breadcrumbs/types`
  110. */
  111. const IconWrapper = styled('div')<
  112. Required<Pick<React.ComponentProps<typeof SvgIcon>, 'color'>>
  113. >`
  114. display: flex;
  115. align-items: center;
  116. justify-content: center;
  117. width: 22px;
  118. height: 22px;
  119. border-radius: 50%;
  120. color: ${p => p.theme.white};
  121. background: ${p => p.theme[p.color] ?? p.color};
  122. box-shadow: ${p => p.theme.dropShadowLightest};
  123. position: relative;
  124. `;
  125. const MemoizedBreadcrumbItem = memo(BreadcrumbItem);
  126. export default MemoizedBreadcrumbItem;