import {Fragment, useCallback, useEffect, useMemo, useState} from 'react'; import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {LinkButton} from 'sentry/components/button'; import {TabList, Tabs} from 'sentry/components/tabs'; import {Tooltip} from 'sentry/components/tooltip'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; import {useLocation} from 'sentry/utils/useLocation'; import useMedia from 'sentry/utils/useMedia'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import {useGroupEvent} from 'sentry/views/issueDetails/useGroupEvent'; import {useDefaultIssueEvent} from 'sentry/views/issueDetails/utils'; const enum EventNavOptions { RECOMMENDED = 'recommended', LATEST = 'latest', OLDEST = 'oldest', CUSTOM = 'custom', } const EventNavOrder = [ EventNavOptions.OLDEST, EventNavOptions.LATEST, EventNavOptions.RECOMMENDED, EventNavOptions.CUSTOM, ]; interface IssueDetailsEventNavigationProps { event: Event | undefined; group: Group; } export function IssueDetailsEventNavigation({ event, group, }: IssueDetailsEventNavigationProps) { const organization = useOrganization(); const location = useLocation(); const params = useParams<{eventId?: string}>(); const theme = useTheme(); const defaultIssueEvent = useDefaultIssueEvent(); const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.small})`); const [shouldPreload, setShouldPreload] = useState({next: false, previous: false}); // Reset shouldPreload when the groupId changes useEffect(() => { setShouldPreload({next: false, previous: false}); }, [group.id]); // Prefetch next useGroupEvent({ groupId: group.id, eventId: event?.nextEventID ?? undefined, options: {enabled: shouldPreload.next}, }); // Prefetch previous useGroupEvent({ groupId: group.id, eventId: event?.previousEventID ?? undefined, options: {enabled: shouldPreload.previous}, }); const handleHoverPagination = useCallback( (direction: 'next' | 'previous', isEnabled: boolean) => () => { if (isEnabled) { setShouldPreload(prev => ({...prev, [direction]: true})); } }, [] ); const selectedOption = useMemo(() => { switch (params.eventId) { case EventNavOptions.RECOMMENDED: case EventNavOptions.LATEST: case EventNavOptions.OLDEST: return params.eventId; case undefined: return defaultIssueEvent; default: return EventNavOptions.CUSTOM; } }, [params.eventId, defaultIssueEvent]); const EventNavLabels = { [EventNavOptions.RECOMMENDED]: isSmallScreen ? t('Rec.') : t('Recommended'), [EventNavOptions.OLDEST]: t('First'), [EventNavOptions.LATEST]: t('Last'), }; const EventNavTooltips = { [EventNavOptions.RECOMMENDED]: t('Recommended event matching filters'), [EventNavOptions.OLDEST]: t('First event matching filters'), [EventNavOptions.LATEST]: t('Last event matching filters'), }; const onTabChange = (tabKey: typeof selectedOption) => { trackAnalytics('issue_details.event_navigation_selected', { organization, content: EventNavLabels[tabKey as keyof typeof EventNavLabels], }); }; const baseEventsPath = `/organizations/${organization.slug}/issues/${group.id}/events/`; const grayText = css` color: ${theme.subText}; font-weight: ${theme.fontWeightNormal}; `; return ( } disabled={!defined(event?.previousEventID)} analyticsEventKey="issue_details.previous_event_clicked" analyticsEventName="Issue Details: Previous Event Clicked" to={{ pathname: `${baseEventsPath}${event?.previousEventID}/`, query: {...location.query, referrer: 'previous-event'}, }} css={grayText} onMouseEnter={handleHoverPagination( 'previous', defined(event?.previousEventID) )} onClick={() => { // Assume they will continue to paginate setShouldPreload({next: true, previous: true}); }} /> } disabled={!defined(event?.nextEventID)} analyticsEventKey="issue_details.next_event_clicked" analyticsEventName="Issue Details: Next Event Clicked" to={{ pathname: `${baseEventsPath}${event?.nextEventID}/`, query: {...location.query, referrer: 'next-event'}, }} css={grayText} onMouseEnter={handleHoverPagination('next', defined(event?.nextEventID))} onClick={() => { // Assume they will continue to paginate setShouldPreload({next: true, previous: true}); }} /> {EventNavOrder.map(label => { const eventPath = label === selectedOption ? undefined : { pathname: normalizeUrl(baseEventsPath + label + '/'), query: {...location.query, referrer: `${label}-event`}, }; return ( ); })} ); } const Navigation = styled('div')` display: flex; padding-right: ${space(0.25)}; border-right: 1px solid ${p => p.theme.gray100}; `;