userActionsNavigator.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Type from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type';
  4. import {
  5. onlyUserActions,
  6. transformCrumbs,
  7. } from 'sentry/components/events/interfaces/breadcrumbs/utils';
  8. import {
  9. Panel as BasePanel,
  10. PanelBody as BasePanelBody,
  11. PanelHeader as BasePanelHeader,
  12. PanelItem,
  13. } from 'sentry/components/panels';
  14. import Placeholder from 'sentry/components/placeholder';
  15. import ActionCategory from 'sentry/components/replays/actionCategory';
  16. import PlayerRelativeTime from 'sentry/components/replays/playerRelativeTime';
  17. import {useReplayContext} from 'sentry/components/replays/replayContext';
  18. import {relativeTimeInMs} from 'sentry/components/replays/utils';
  19. import {t} from 'sentry/locale';
  20. import space from 'sentry/styles/space';
  21. import {Crumb, RawCrumb} from 'sentry/types/breadcrumbs';
  22. import {EventTransaction} from 'sentry/types/event';
  23. import {getCurrentUserAction} from 'sentry/utils/replays/getCurrentUserAction';
  24. function CrumbPlaceholder({number}: {number: number}) {
  25. return (
  26. <Fragment>
  27. {[...Array(number)].map((_, i) => (
  28. <PlaceholderMargin key={i} height="40px" />
  29. ))}
  30. </Fragment>
  31. );
  32. }
  33. type Props = {
  34. /**
  35. * Raw breadcrumbs, `undefined` means it is still loading
  36. */
  37. crumbs: RawCrumb[] | undefined;
  38. /**
  39. * Root replay event, `undefined` means it is still loading
  40. */
  41. event: EventTransaction | undefined;
  42. };
  43. type ContainerProps = {
  44. isHovered: boolean;
  45. isSelected: boolean;
  46. };
  47. function UserActionsNavigator({event, crumbs}: Props) {
  48. const {setCurrentTime, currentHoverTime} = useReplayContext();
  49. const [currentUserAction, setCurrentUserAction] = useState<Crumb>();
  50. const [closestUserAction, setClosestUserAction] = useState<Crumb>();
  51. const {startTimestamp} = event || {};
  52. const userActionCrumbs = crumbs && onlyUserActions(transformCrumbs(crumbs));
  53. const isLoaded = userActionCrumbs && startTimestamp;
  54. const getClosestUserAction = useCallback(
  55. async (hovertime: number) => {
  56. const closestUserActionItem = getCurrentUserAction(
  57. userActionCrumbs,
  58. startTimestamp,
  59. hovertime
  60. );
  61. if (
  62. closestUserActionItem &&
  63. closestUserAction?.timestamp !== closestUserActionItem.timestamp
  64. ) {
  65. setClosestUserAction(closestUserActionItem);
  66. }
  67. },
  68. [closestUserAction?.timestamp, startTimestamp, userActionCrumbs]
  69. );
  70. useEffect(() => {
  71. if (!currentHoverTime) {
  72. setClosestUserAction(undefined);
  73. return;
  74. }
  75. getClosestUserAction(currentHoverTime);
  76. }, [getClosestUserAction, currentHoverTime]);
  77. return (
  78. <Panel>
  79. <PanelHeader>{t('Event Chapters')}</PanelHeader>
  80. <PanelBody>
  81. {!isLoaded && <CrumbPlaceholder number={4} />}
  82. {isLoaded &&
  83. userActionCrumbs.map(item => (
  84. <PanelItemCenter
  85. key={item.id}
  86. onClick={() => {
  87. setCurrentUserAction(item);
  88. item.timestamp
  89. ? setCurrentTime(relativeTimeInMs(item.timestamp, startTimestamp))
  90. : '';
  91. }}
  92. >
  93. <Container
  94. isHovered={closestUserAction?.id === item.id}
  95. isSelected={currentUserAction?.id === item.id}
  96. >
  97. <Wrapper>
  98. <Type
  99. type={item.type}
  100. color={item.color}
  101. description={item.description}
  102. />
  103. <ActionCategory category={item} />
  104. </Wrapper>
  105. <PlayerRelativeTime
  106. relativeTime={startTimestamp}
  107. timestamp={item.timestamp}
  108. />
  109. </Container>
  110. </PanelItemCenter>
  111. ))}
  112. </PanelBody>
  113. </Panel>
  114. );
  115. }
  116. // FYI: Since the Replay Player has dynamic height based
  117. // on the width of the window,
  118. // height: 0; will helps us to reset the height
  119. // min-height: 100%; will helps us to grow at the same height of Player
  120. const Panel = styled(BasePanel)`
  121. width: 100%;
  122. display: grid;
  123. grid-template-rows: auto 1fr;
  124. height: 0;
  125. min-height: 100%;
  126. @media only screen and (max-width: ${p => p.theme.breakpoints[1]}) {
  127. min-height: 450px;
  128. }
  129. `;
  130. const PanelHeader = styled(BasePanelHeader)`
  131. background-color: ${p => p.theme.white};
  132. border-bottom: none;
  133. font-size: ${p => p.theme.fontSizeSmall};
  134. color: ${p => p.theme.gray300};
  135. text-transform: capitalize;
  136. padding: ${space(1.5)} ${space(2)} ${space(0.5)};
  137. `;
  138. const PanelBody = styled(BasePanelBody)`
  139. overflow-y: auto;
  140. `;
  141. const PanelItemCenter = styled(PanelItem)`
  142. display: block;
  143. padding: ${space(0)};
  144. cursor: pointer;
  145. `;
  146. const Container = styled('div')<ContainerProps>`
  147. display: flex;
  148. justify-content: space-between;
  149. align-items: center;
  150. border-left: 4px solid transparent;
  151. padding: ${space(1)} ${space(1.5)};
  152. &:hover {
  153. background: ${p => p.theme.surface400};
  154. }
  155. ${p => p.isHovered && `background: ${p.theme.surface400};`}
  156. ${p => p.isSelected && `border-left: 4px solid ${p.theme.purple300};`}
  157. `;
  158. const Wrapper = styled('div')`
  159. display: flex;
  160. align-items: center;
  161. gap: ${space(1)};
  162. font-size: ${p => p.theme.fontSizeMedium};
  163. color: ${p => p.theme.gray500};
  164. `;
  165. const PlaceholderMargin = styled(Placeholder)`
  166. margin: ${space(1)} ${space(1.5)};
  167. width: auto;
  168. `;
  169. export default UserActionsNavigator;