traceTimeline.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import {useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import ErrorBoundary from 'sentry/components/errorBoundary';
  4. import Placeholder from 'sentry/components/placeholder';
  5. import QuestionTooltip from 'sentry/components/questionTooltip';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import type {Event} from 'sentry/types';
  9. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  10. import {useDimensions} from 'sentry/utils/useDimensions';
  11. import {TraceTimelineEvents} from './traceTimelineEvents';
  12. import {useTraceTimelineEvents} from './useTraceTimelineEvents';
  13. interface TraceTimelineProps {
  14. event: Event;
  15. }
  16. export function TraceTimeline({event}: TraceTimelineProps) {
  17. const timelineRef = useRef<HTMLDivElement>(null);
  18. const {width} = useDimensions({elementRef: timelineRef});
  19. const {isError, isLoading, traceEvents} = useTraceTimelineEvents({event});
  20. const hasTraceId = !!event.contexts?.trace?.trace_id;
  21. let timelineStatus: string | undefined;
  22. if (hasTraceId && !isLoading) {
  23. timelineStatus = traceEvents.length > 1 ? 'shown' : 'empty';
  24. } else if (!hasTraceId) {
  25. timelineStatus = 'no_trace_id';
  26. }
  27. useRouteAnalyticsParams(timelineStatus ? {trace_timeline_status: timelineStatus} : {});
  28. if (!hasTraceId) {
  29. return null;
  30. }
  31. const noEvents = !isLoading && traceEvents.length === 0;
  32. // Timelines with only the current event are not useful
  33. const onlySelfEvent =
  34. !isLoading &&
  35. traceEvents.length > 0 &&
  36. traceEvents.every(item => item.id === event.id);
  37. if (isError || noEvents || onlySelfEvent) {
  38. // display empty placeholder to reduce layout shift
  39. return <div style={{height: 36}} data-test-id="trace-timeline-empty" />;
  40. }
  41. return (
  42. <ErrorBoundary mini>
  43. <TimelineWrapper>
  44. <div ref={timelineRef}>
  45. {isLoading ? (
  46. <LoadingSkeleton>
  47. <Placeholder height="14px" />
  48. <Placeholder height="8px" />
  49. </LoadingSkeleton>
  50. ) : (
  51. <TimelineEventsContainer>
  52. <TimelineOutline />
  53. {/* Sets a min width of 200 for testing */}
  54. <TraceTimelineEvents event={event} width={Math.max(width, 200)} />
  55. </TimelineEventsContainer>
  56. )}
  57. </div>
  58. <QuestionTooltipWrapper>
  59. <QuestionTooltip
  60. size="sm"
  61. title={t(
  62. 'This is a trace timeline showing all related events happening upstream and downstream of this event'
  63. )}
  64. position="bottom"
  65. />
  66. </QuestionTooltipWrapper>
  67. </TimelineWrapper>
  68. </ErrorBoundary>
  69. );
  70. }
  71. const TimelineWrapper = styled('div')`
  72. display: grid;
  73. grid-template-columns: 1fr auto;
  74. align-items: start;
  75. gap: ${space(2)};
  76. margin-top: ${space(0.25)};
  77. `;
  78. const QuestionTooltipWrapper = styled('div')`
  79. margin-top: ${space(0.25)};
  80. `;
  81. /**
  82. * Displays the container the dots appear inside of
  83. */
  84. const TimelineOutline = styled('div')`
  85. position: absolute;
  86. left: 0;
  87. top: 3.5px;
  88. width: 100%;
  89. height: 10px;
  90. border: 1px solid ${p => p.theme.innerBorder};
  91. border-radius: ${p => p.theme.borderRadius};
  92. background-color: ${p => p.theme.backgroundSecondary};
  93. `;
  94. const TimelineEventsContainer = styled('div')`
  95. position: relative;
  96. height: 34px;
  97. padding-top: 10px;
  98. `;
  99. const LoadingSkeleton = styled('div')`
  100. display: flex;
  101. flex-direction: column;
  102. gap: ${space(0.25)};
  103. padding: ${space(0.5)} 0 ${space(1)};
  104. height: 34px;
  105. `;