index.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import BreadcrumbIcon from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type/icon';
  4. import {PanelTable} from 'sentry/components/panels';
  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 {t} from 'sentry/locale';
  10. import space from 'sentry/styles/space';
  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 startTimestamp = replay.getEvent().startTimestamp;
  19. return (
  20. <Fragment>
  21. <StyledPanelTable
  22. isEmpty={actions.length === 0}
  23. emptyMessage={t('No DOM actions found.')}
  24. isLoading={isLoading}
  25. headers={[t('Action'), t('Selector'), t('HTML'), t('Timestamp')]}
  26. >
  27. {actions.map((mutation, i) => (
  28. <Fragment key={i}>
  29. <TitleContainer>
  30. <IconWrapper color={mutation.crumb.color}>
  31. <BreadcrumbIcon type={mutation.crumb.type} />
  32. </IconWrapper>
  33. <Title>{getDetails(mutation.crumb).title}</Title>
  34. </TitleContainer>
  35. <Column>
  36. <Truncate
  37. maxLength={30}
  38. leftTrim={(mutation.crumb.message || '').includes('>')}
  39. value={mutation.crumb.message || ''}
  40. />
  41. </Column>
  42. <Column>
  43. <HTMLCode>{mutation.html}</HTMLCode>
  44. </Column>
  45. <Column>
  46. <PlayerRelativeTime
  47. relativeTime={startTimestamp}
  48. timestamp={mutation.crumb.timestamp}
  49. />
  50. {}
  51. </Column>
  52. </Fragment>
  53. ))}
  54. </StyledPanelTable>
  55. </Fragment>
  56. );
  57. }
  58. const StyledPanelTable = styled(PanelTable)`
  59. grid-template-columns: max-content max-content 1fr max-content;
  60. font-size: ${p => p.theme.fontSizeSmall};
  61. `;
  62. const Column = styled('div')`
  63. display: flex;
  64. align-items: flex-start;
  65. `;
  66. /**
  67. * Taken `from events/interfaces/.../breadcrumbs/types`
  68. */
  69. const IconWrapper = styled('div')<Required<Pick<SVGIconProps, 'color'>>>`
  70. display: flex;
  71. align-items: center;
  72. justify-content: center;
  73. width: 24px;
  74. height: 24px;
  75. border-radius: 50%;
  76. color: ${p => p.theme.white};
  77. background: ${p => p.theme[p.color] ?? p.color};
  78. box-shadow: ${p => p.theme.dropShadowLightest};
  79. `;
  80. const TitleContainer = styled('div')`
  81. display: flex;
  82. justify-content: space-between;
  83. gap: ${space(1)};
  84. `;
  85. const Title = styled('span')`
  86. ${p => p.theme.overflowEllipsis};
  87. text-transform: capitalize;
  88. color: ${p => p.theme.gray400};
  89. line-height: ${p => p.theme.text.lineHeightBody};
  90. `;
  91. const HTMLCode = styled(
  92. ({children, className}: {children: string; className?: string}) => (
  93. <textarea className={className} readOnly value={children} />
  94. )
  95. )`
  96. border: none;
  97. width: 100%;
  98. resize: vertical;
  99. font-family: ${p => p.theme.text.familyMono};
  100. `;
  101. export default DomMutations;