index.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import BreadcrumbIcon from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type/icon';
  4. import HTMLCode from 'sentry/components/htmlCode';
  5. import {getDetails} from 'sentry/components/replays/breadcrumbs/utils';
  6. import PlayerRelativeTime from 'sentry/components/replays/playerRelativeTime';
  7. import Truncate from 'sentry/components/truncate';
  8. import {SVGIconProps} from 'sentry/icons/svgIcon';
  9. import space from 'sentry/styles/space';
  10. import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
  11. import useExtractedCrumbHtml from 'sentry/utils/replays/hooks/useExtractedCrumbHtml';
  12. import type ReplayReader from 'sentry/utils/replays/replayReader';
  13. type Props = {
  14. replay: ReplayReader;
  15. };
  16. function DomMutations({replay}: Props) {
  17. const {isLoading, actions} = useExtractedCrumbHtml({replay});
  18. const startTimestampMs = replay.getReplay().startedAt.getTime();
  19. const {handleMouseEnter, handleMouseLeave, handleClick} =
  20. useCrumbHandlers(startTimestampMs);
  21. if (isLoading) {
  22. return null;
  23. }
  24. return (
  25. <MutationList>
  26. {actions.map((mutation, i) => (
  27. <MutationListItem
  28. key={i}
  29. onMouseEnter={() => handleMouseEnter(mutation.crumb)}
  30. onMouseLeave={() => handleMouseLeave(mutation.crumb)}
  31. >
  32. <StepConnector />
  33. <MutationItemContainer>
  34. <div>
  35. <MutationMetadata>
  36. <IconWrapper color={mutation.crumb.color}>
  37. <BreadcrumbIcon type={mutation.crumb.type} />
  38. </IconWrapper>
  39. <UnstyledButton onClick={() => handleClick(mutation.crumb)}>
  40. <PlayerRelativeTime
  41. relativeTimeMs={startTimestampMs}
  42. timestamp={mutation.crumb.timestamp}
  43. />
  44. </UnstyledButton>
  45. </MutationMetadata>
  46. <MutationDetails>
  47. <TitleContainer>
  48. <Title>{getDetails(mutation.crumb).title}</Title>
  49. </TitleContainer>
  50. <Truncate
  51. maxLength={30}
  52. leftTrim={(mutation.crumb.message || '').includes('>')}
  53. value={mutation.crumb.message || ''}
  54. />
  55. </MutationDetails>
  56. </div>
  57. <CodeContainer>
  58. <HTMLCode code={mutation.html} />
  59. </CodeContainer>
  60. </MutationItemContainer>
  61. </MutationListItem>
  62. ))}
  63. </MutationList>
  64. );
  65. }
  66. const MutationList = styled('ul')`
  67. list-style: none;
  68. position: relative;
  69. height: 100%;
  70. overflow-y: auto;
  71. border: 1px solid ${p => p.theme.border};
  72. border-radius: ${p => p.theme.borderRadius};
  73. padding-left: 0;
  74. margin-bottom: 0;
  75. `;
  76. const MutationListItem = styled('li')`
  77. display: flex;
  78. align-items: start;
  79. padding: ${space(2)};
  80. &:hover {
  81. background-color: ${p => p.theme.backgroundSecondary};
  82. }
  83. `;
  84. const MutationItemContainer = styled('div')`
  85. display: grid;
  86. grid-template-columns: 280px 1fr;
  87. `;
  88. const MutationMetadata = styled('div')`
  89. display: flex;
  90. align-items: start;
  91. column-gap: ${space(1)};
  92. `;
  93. /**
  94. * Taken `from events/interfaces/.../breadcrumbs/types`
  95. */
  96. const IconWrapper = styled('div')<Required<Pick<SVGIconProps, 'color'>>>`
  97. display: flex;
  98. align-items: center;
  99. justify-content: center;
  100. width: 24px;
  101. height: 24px;
  102. border-radius: 50%;
  103. color: ${p => p.theme.white};
  104. background: ${p => p.theme[p.color] ?? p.color};
  105. box-shadow: ${p => p.theme.dropShadowLightest};
  106. z-index: 1;
  107. `;
  108. const UnstyledButton = styled('button')`
  109. background: none;
  110. border: none;
  111. padding: 0;
  112. `;
  113. const MutationDetails = styled('div')`
  114. margin-left: 30px;
  115. margin-top: ${space(0.5)};
  116. margin-bottom: ${space(3)};
  117. `;
  118. const TitleContainer = styled('div')`
  119. display: flex;
  120. justify-content: space-between;
  121. gap: ${space(1)};
  122. `;
  123. const Title = styled('span')`
  124. ${p => p.theme.overflowEllipsis};
  125. text-transform: capitalize;
  126. color: ${p => p.theme.gray400};
  127. font-weight: bold;
  128. line-height: ${p => p.theme.text.lineHeightBody};
  129. margin-bottom: ${space(0.5)};
  130. `;
  131. const CodeContainer = styled('div')`
  132. overflow: auto;
  133. max-height: 400px;
  134. max-width: 100%;
  135. `;
  136. const StepConnector = styled('div')`
  137. position: absolute;
  138. height: 100%;
  139. top: 28px;
  140. left: 31px;
  141. border-right: 1px ${p => p.theme.border} dashed;
  142. `;
  143. export default DomMutations;