issueDetailsEventNavigation.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import {css, useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {LinkButton} from 'sentry/components/button';
  5. import {TabList, Tabs} from 'sentry/components/tabs';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {IconChevron} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {Event} from 'sentry/types/event';
  11. import type {Group} from 'sentry/types/group';
  12. import {defined} from 'sentry/utils';
  13. import {trackAnalytics} from 'sentry/utils/analytics';
  14. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  15. import {useLocation} from 'sentry/utils/useLocation';
  16. import useMedia from 'sentry/utils/useMedia';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {useParams} from 'sentry/utils/useParams';
  19. import {useGroupEvent} from 'sentry/views/issueDetails/useGroupEvent';
  20. import {useDefaultIssueEvent} from 'sentry/views/issueDetails/utils';
  21. const enum EventNavOptions {
  22. RECOMMENDED = 'recommended',
  23. LATEST = 'latest',
  24. OLDEST = 'oldest',
  25. CUSTOM = 'custom',
  26. }
  27. const EventNavOrder = [
  28. EventNavOptions.OLDEST,
  29. EventNavOptions.LATEST,
  30. EventNavOptions.RECOMMENDED,
  31. EventNavOptions.CUSTOM,
  32. ];
  33. interface IssueDetailsEventNavigationProps {
  34. event: Event | undefined;
  35. group: Group;
  36. }
  37. export function IssueDetailsEventNavigation({
  38. event,
  39. group,
  40. }: IssueDetailsEventNavigationProps) {
  41. const organization = useOrganization();
  42. const location = useLocation();
  43. const params = useParams<{eventId?: string}>();
  44. const theme = useTheme();
  45. const defaultIssueEvent = useDefaultIssueEvent();
  46. const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.small})`);
  47. const [shouldPreload, setShouldPreload] = useState({next: false, previous: false});
  48. // Reset shouldPreload when the groupId changes
  49. useEffect(() => {
  50. setShouldPreload({next: false, previous: false});
  51. }, [group.id]);
  52. // Prefetch next
  53. useGroupEvent({
  54. groupId: group.id,
  55. eventId: event?.nextEventID ?? undefined,
  56. options: {enabled: shouldPreload.next},
  57. });
  58. // Prefetch previous
  59. useGroupEvent({
  60. groupId: group.id,
  61. eventId: event?.previousEventID ?? undefined,
  62. options: {enabled: shouldPreload.previous},
  63. });
  64. const handleHoverPagination = useCallback(
  65. (direction: 'next' | 'previous', isEnabled: boolean) => () => {
  66. if (isEnabled) {
  67. setShouldPreload(prev => ({...prev, [direction]: true}));
  68. }
  69. },
  70. []
  71. );
  72. const selectedOption = useMemo(() => {
  73. switch (params.eventId) {
  74. case EventNavOptions.RECOMMENDED:
  75. case EventNavOptions.LATEST:
  76. case EventNavOptions.OLDEST:
  77. return params.eventId;
  78. case undefined:
  79. return defaultIssueEvent;
  80. default:
  81. return EventNavOptions.CUSTOM;
  82. }
  83. }, [params.eventId, defaultIssueEvent]);
  84. const EventNavLabels = {
  85. [EventNavOptions.RECOMMENDED]: isSmallScreen ? t('Rec.') : t('Recommended'),
  86. [EventNavOptions.OLDEST]: t('First'),
  87. [EventNavOptions.LATEST]: t('Last'),
  88. [EventNavOptions.CUSTOM]: t('Custom'),
  89. };
  90. const EventNavTooltips = {
  91. [EventNavOptions.RECOMMENDED]: t('Recent event with richer content'),
  92. [EventNavOptions.OLDEST]: t('First event matching filters'),
  93. [EventNavOptions.LATEST]: t('Last event matching filters'),
  94. };
  95. const onTabChange = (tabKey: typeof selectedOption) => {
  96. trackAnalytics('issue_details.event_navigation_selected', {
  97. organization,
  98. content: EventNavLabels[tabKey as keyof typeof EventNavLabels],
  99. });
  100. };
  101. const baseEventsPath = `/organizations/${organization.slug}/issues/${group.id}/events/`;
  102. const grayText = css`
  103. color: ${theme.subText};
  104. font-weight: ${theme.fontWeightNormal};
  105. `;
  106. return (
  107. <Fragment>
  108. <Navigation>
  109. <Tooltip title={t('Previous Event')} skipWrapper>
  110. <LinkButton
  111. aria-label={t('Previous Event')}
  112. borderless
  113. size="xs"
  114. icon={<IconChevron direction="left" />}
  115. disabled={!defined(event?.previousEventID)}
  116. analyticsEventKey="issue_details.previous_event_clicked"
  117. analyticsEventName="Issue Details: Previous Event Clicked"
  118. to={{
  119. pathname: `${baseEventsPath}${event?.previousEventID}/`,
  120. query: {...location.query, referrer: 'previous-event'},
  121. }}
  122. css={grayText}
  123. onMouseEnter={handleHoverPagination(
  124. 'previous',
  125. defined(event?.previousEventID)
  126. )}
  127. onClick={() => {
  128. // Assume they will continue to paginate
  129. setShouldPreload({next: true, previous: true});
  130. }}
  131. />
  132. </Tooltip>
  133. <Tooltip title={t('Next Event')} skipWrapper>
  134. <LinkButton
  135. aria-label={t('Next Event')}
  136. borderless
  137. size="xs"
  138. icon={<IconChevron direction="right" />}
  139. disabled={!defined(event?.nextEventID)}
  140. analyticsEventKey="issue_details.next_event_clicked"
  141. analyticsEventName="Issue Details: Next Event Clicked"
  142. to={{
  143. pathname: `${baseEventsPath}${event?.nextEventID}/`,
  144. query: {...location.query, referrer: 'next-event'},
  145. }}
  146. css={grayText}
  147. onMouseEnter={handleHoverPagination('next', defined(event?.nextEventID))}
  148. onClick={() => {
  149. // Assume they will continue to paginate
  150. setShouldPreload({next: true, previous: true});
  151. }}
  152. />
  153. </Tooltip>
  154. </Navigation>
  155. <Tabs value={selectedOption} disableOverflow onChange={onTabChange}>
  156. <TabList hideBorder variant="floating">
  157. {EventNavOrder.map(label => {
  158. const eventPath =
  159. label === selectedOption
  160. ? undefined
  161. : {
  162. pathname: normalizeUrl(baseEventsPath + label + '/'),
  163. query: {...location.query, referrer: `${label}-event`},
  164. };
  165. return (
  166. <TabList.Item
  167. to={eventPath}
  168. key={label}
  169. hidden={label === EventNavOptions.CUSTOM}
  170. textValue={EventNavLabels[label as keyof typeof EventNavLabels]}
  171. >
  172. <Tooltip
  173. title={EventNavTooltips[label as keyof typeof EventNavTooltips]}
  174. skipWrapper
  175. >
  176. {EventNavLabels[label as keyof typeof EventNavLabels]}
  177. </Tooltip>
  178. </TabList.Item>
  179. );
  180. })}
  181. </TabList>
  182. </Tabs>
  183. </Fragment>
  184. );
  185. }
  186. const Navigation = styled('div')`
  187. display: flex;
  188. padding-right: ${space(0.25)};
  189. border-right: 1px solid ${p => p.theme.gray100};
  190. `;