breadcrumbsDataSection.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import {useCallback, useMemo, useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  4. import {Button} from 'sentry/components/button';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import ErrorBoundary from 'sentry/components/errorBoundary';
  7. import {
  8. BreadcrumbControlOptions,
  9. BreadcrumbsDrawer,
  10. } from 'sentry/components/events/breadcrumbs/breadcrumbsDrawer';
  11. import BreadcrumbsTimeline from 'sentry/components/events/breadcrumbs/breadcrumbsTimeline';
  12. import {
  13. BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY,
  14. BREADCRUMB_TIME_DISPLAY_OPTIONS,
  15. BreadcrumbTimeDisplay,
  16. getEnhancedBreadcrumbs,
  17. getSummaryBreadcrumbs,
  18. } from 'sentry/components/events/breadcrumbs/utils';
  19. import {
  20. BREADCRUMB_SORT_LOCALSTORAGE_KEY,
  21. BreadcrumbSort,
  22. } from 'sentry/components/events/interfaces/breadcrumbs';
  23. import useDrawer from 'sentry/components/globalDrawer';
  24. import {IconClock, IconEllipsis, IconSearch, IconTimer} from 'sentry/icons';
  25. import {t, tct} from 'sentry/locale';
  26. import {space} from 'sentry/styles/space';
  27. import type {Event} from 'sentry/types/event';
  28. import type {Group} from 'sentry/types/group';
  29. import type {Project} from 'sentry/types/project';
  30. import {trackAnalytics} from 'sentry/utils/analytics';
  31. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  32. import useOrganization from 'sentry/utils/useOrganization';
  33. import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
  34. import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
  35. interface BreadcrumbsDataSectionProps {
  36. event: Event;
  37. group: Group;
  38. project: Project;
  39. }
  40. export default function BreadcrumbsDataSection({
  41. event,
  42. group,
  43. project,
  44. }: BreadcrumbsDataSectionProps) {
  45. const viewAllButtonRef = useRef<HTMLButtonElement>(null);
  46. const [container, setContainer] = useState<HTMLDivElement | null>(null);
  47. const {closeDrawer, isDrawerOpen, openDrawer} = useDrawer();
  48. const organization = useOrganization();
  49. const [timeDisplay, setTimeDisplay] = useLocalStorageState<BreadcrumbTimeDisplay>(
  50. BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY,
  51. BreadcrumbTimeDisplay.ABSOLUTE
  52. );
  53. // Use the local storage preferences, but allow the drawer to do updates
  54. const [sort, _setSort] = useLocalStorageState<BreadcrumbSort>(
  55. BREADCRUMB_SORT_LOCALSTORAGE_KEY,
  56. BreadcrumbSort.NEWEST
  57. );
  58. const enhancedCrumbs = useMemo(() => getEnhancedBreadcrumbs(event), [event]);
  59. const summaryCrumbs = useMemo(
  60. () => getSummaryBreadcrumbs(enhancedCrumbs, sort),
  61. [enhancedCrumbs, sort]
  62. );
  63. const startTimeString = useMemo(
  64. () =>
  65. timeDisplay === BreadcrumbTimeDisplay.RELATIVE
  66. ? summaryCrumbs?.at(0)?.breadcrumb?.timestamp
  67. : undefined,
  68. [summaryCrumbs, timeDisplay]
  69. );
  70. const onViewAllBreadcrumbs = useCallback(
  71. (focusControl?: BreadcrumbControlOptions) => {
  72. trackAnalytics('breadcrumbs.issue_details.drawer_opened', {
  73. control: focusControl ?? 'view all',
  74. organization,
  75. });
  76. openDrawer(
  77. () => (
  78. <BreadcrumbsDrawer
  79. breadcrumbs={enhancedCrumbs}
  80. focusControl={focusControl}
  81. project={project}
  82. event={event}
  83. group={group}
  84. />
  85. ),
  86. {
  87. ariaLabel: 'breadcrumb drawer',
  88. // We prevent a click on the 'View All' button from closing the drawer so that
  89. // we don't reopen it immediately, and instead let the button handle this itself.
  90. shouldCloseOnInteractOutside: element => {
  91. const viewAllButton = viewAllButtonRef.current;
  92. if (viewAllButton?.contains(element)) {
  93. return false;
  94. }
  95. return true;
  96. },
  97. transitionProps: {stiffness: 1000},
  98. }
  99. );
  100. },
  101. [group, event, project, openDrawer, enhancedCrumbs, organization]
  102. );
  103. if (enhancedCrumbs.length === 0) {
  104. return null;
  105. }
  106. const nextTimeDisplay =
  107. timeDisplay === BreadcrumbTimeDisplay.ABSOLUTE
  108. ? BreadcrumbTimeDisplay.RELATIVE
  109. : BreadcrumbTimeDisplay.ABSOLUTE;
  110. const actions = (
  111. <ButtonBar gap={1}>
  112. <Button
  113. aria-label={t('Open Breadcrumb Search')}
  114. icon={<IconSearch size="xs" />}
  115. size="xs"
  116. title={t('Open Search')}
  117. onClick={() => onViewAllBreadcrumbs(BreadcrumbControlOptions.SEARCH)}
  118. />
  119. <Button
  120. aria-label={t('Change Time Format for Breadcrumbs')}
  121. title={tct('Use [format] Timestamps', {
  122. format: BREADCRUMB_TIME_DISPLAY_OPTIONS[nextTimeDisplay].label,
  123. })}
  124. icon={
  125. timeDisplay === BreadcrumbTimeDisplay.ABSOLUTE ? (
  126. <IconClock size="xs" />
  127. ) : (
  128. <IconTimer size="xs" />
  129. )
  130. }
  131. onClick={() => {
  132. setTimeDisplay(nextTimeDisplay);
  133. trackAnalytics('breadcrumbs.issue_details.change_time_display', {
  134. value: nextTimeDisplay,
  135. organization,
  136. });
  137. }}
  138. size="xs"
  139. />
  140. </ButtonBar>
  141. );
  142. const hasViewAll = summaryCrumbs.length !== enhancedCrumbs.length;
  143. return (
  144. <InterimSection
  145. key="breadcrumbs"
  146. type={SectionKey.BREADCRUMBS}
  147. title={
  148. <GuideAnchor target="breadcrumbs" position="top">
  149. {t('Breadcrumbs')}
  150. </GuideAnchor>
  151. }
  152. data-test-id="breadcrumbs-data-section"
  153. actions={actions}
  154. >
  155. <ErrorBoundary mini message={t('There was an error loading the event breadcrumbs')}>
  156. <div ref={setContainer}>
  157. <BreadcrumbsTimeline
  158. breadcrumbs={summaryCrumbs}
  159. startTimeString={startTimeString}
  160. // We want the timeline to appear connected to the 'View All' button
  161. showLastLine={hasViewAll}
  162. fullyExpanded={false}
  163. containerElement={container}
  164. />
  165. </div>
  166. {hasViewAll && (
  167. <ViewAllContainer>
  168. <VerticalEllipsis />
  169. <div>
  170. <ViewAllButton
  171. size="sm"
  172. // Since we've disabled the button as an 'outside click' for the drawer we can change
  173. // the operation based on the drawer state.
  174. onClick={() => (isDrawerOpen ? closeDrawer() : onViewAllBreadcrumbs())}
  175. aria-label={t('View All Breadcrumbs')}
  176. ref={viewAllButtonRef}
  177. >
  178. {t('View All')}
  179. </ViewAllButton>
  180. </div>
  181. </ViewAllContainer>
  182. )}
  183. </ErrorBoundary>
  184. </InterimSection>
  185. );
  186. }
  187. const ViewAllContainer = styled('div')`
  188. position: relative;
  189. display: grid;
  190. grid-template-columns: auto 1fr;
  191. margin-top: ${space(1)};
  192. &::after {
  193. content: '';
  194. position: absolute;
  195. left: 10.5px;
  196. width: 1px;
  197. top: -${space(1)};
  198. height: ${space(1)};
  199. background: ${p => p.theme.border};
  200. }
  201. `;
  202. const VerticalEllipsis = styled(IconEllipsis)`
  203. height: 22px;
  204. color: ${p => p.theme.subText};
  205. margin: ${space(0.5)};
  206. transform: rotate(90deg);
  207. `;
  208. const ViewAllButton = styled(Button)`
  209. padding: ${space(0.75)} ${space(1)};
  210. `;