gridLines.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import {useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import {mergeRefs} from '@react-aria/utils';
  4. import moment from 'moment';
  5. import {updateDateTime} from 'sentry/actionCreators/pageFilters';
  6. import {DateTime} from 'sentry/components/dateTime';
  7. import {space} from 'sentry/styles/space';
  8. import useRouter from 'sentry/utils/useRouter';
  9. import type {TimeWindowConfig} from 'sentry/views/monitors/components/overviewTimeline/types';
  10. import {useTimelineCursor} from './timelineCursor';
  11. import {useTimelineZoom} from './timelineZoom';
  12. import {alignDateToBoundary} from './utils';
  13. interface Props {
  14. timeWindowConfig: TimeWindowConfig;
  15. /**
  16. * The size of the timeline
  17. */
  18. width: number;
  19. /**
  20. * Enable zoom selection
  21. */
  22. allowZoom?: boolean;
  23. className?: string;
  24. /**
  25. * Enable the timeline cursor
  26. */
  27. showCursor?: boolean;
  28. /**
  29. * Enabling causes the cursor tooltip to stick to the top of the viewport.
  30. */
  31. stickyCursor?: boolean;
  32. }
  33. interface TimeMarker {
  34. date: Date;
  35. /**
  36. * Props to pass to the DateTime component
  37. */
  38. dateTimeProps: TimeWindowConfig['dateTimeProps'];
  39. /**
  40. * The position in pixels of the tick
  41. */
  42. position: number;
  43. }
  44. function getTimeMarkersFromConfig(config: TimeWindowConfig, width: number) {
  45. const {start, end, elapsedMinutes, intervals, dateTimeProps} = config;
  46. const {referenceMarkerInterval, minimumMarkerInterval, normalMarkerInterval} =
  47. intervals;
  48. const msPerPixel = (elapsedMinutes * 60 * 1000) / width;
  49. // The first marker will always be the starting time. This always renders the
  50. // full date and time
  51. const markers: TimeMarker[] = [
  52. {
  53. date: start,
  54. position: 0,
  55. dateTimeProps: {timeZone: true},
  56. },
  57. ];
  58. // The mark after the first mark will be aligned to a boundary to make it
  59. // easier to understand the rest of the marks
  60. const currentMark = alignDateToBoundary(moment(start), normalMarkerInterval);
  61. // The first label is larger since we include the date, time, and timezone.
  62. while (currentMark.isBefore(moment(start).add(referenceMarkerInterval, 'minutes'))) {
  63. currentMark.add(normalMarkerInterval, 'minute');
  64. }
  65. // Generate time markers which represent location of grid lines/time labels.
  66. // Stop adding markers once there's no more room for more markers
  67. while (moment(currentMark).add(minimumMarkerInterval, 'minutes').isBefore(end)) {
  68. const position = (currentMark.valueOf() - start.valueOf()) / msPerPixel;
  69. markers.push({date: currentMark.toDate(), position, dateTimeProps});
  70. currentMark.add(normalMarkerInterval, 'minutes');
  71. }
  72. return markers;
  73. }
  74. export function GridLineTimeLabels({width, timeWindowConfig, className}: Props) {
  75. const markers = getTimeMarkersFromConfig(timeWindowConfig, width);
  76. return (
  77. <LabelsContainer className={className}>
  78. {markers.map(({date, position, dateTimeProps}) => (
  79. <TimeLabelContainer key={date.getTime()} left={position}>
  80. <TimeLabel date={date} {...dateTimeProps} />
  81. </TimeLabelContainer>
  82. ))}
  83. </LabelsContainer>
  84. );
  85. }
  86. export function GridLineOverlay({
  87. width,
  88. timeWindowConfig,
  89. showCursor,
  90. stickyCursor,
  91. allowZoom,
  92. className,
  93. }: Props) {
  94. const router = useRouter();
  95. const {start, dateLabelFormat} = timeWindowConfig;
  96. const msPerPixel = (timeWindowConfig.elapsedMinutes * 60 * 1000) / width;
  97. const dateFromPosition = useCallback(
  98. (position: number) => moment(start.getTime() + msPerPixel * position),
  99. [msPerPixel, start]
  100. );
  101. const makeCursorLabel = useCallback(
  102. (position: number) => dateFromPosition(position).format(dateLabelFormat),
  103. [dateFromPosition, dateLabelFormat]
  104. );
  105. const handleZoom = useCallback(
  106. (startX: number, endX: number) =>
  107. updateDateTime(
  108. {
  109. start: dateFromPosition(startX).toDate(),
  110. end: dateFromPosition(endX).toDate(),
  111. },
  112. router
  113. ),
  114. [dateFromPosition, router]
  115. );
  116. const {
  117. selectionContainerRef,
  118. timelineSelector,
  119. isActive: selectionIsActive,
  120. } = useTimelineZoom<HTMLDivElement>({enabled: !!allowZoom, onSelect: handleZoom});
  121. const {cursorContainerRef, timelineCursor} = useTimelineCursor<HTMLDivElement>({
  122. enabled: showCursor && !selectionIsActive,
  123. sticky: stickyCursor,
  124. labelText: makeCursorLabel,
  125. });
  126. const overlayRef = mergeRefs(cursorContainerRef, selectionContainerRef);
  127. const markers = getTimeMarkersFromConfig(timeWindowConfig, width);
  128. return (
  129. <Overlay ref={overlayRef} className={className}>
  130. {timelineCursor}
  131. {timelineSelector}
  132. <GridLineContainer>
  133. {markers.map(({date, position}) => (
  134. <Gridline key={date.getTime()} left={position} />
  135. ))}
  136. </GridLineContainer>
  137. </Overlay>
  138. );
  139. }
  140. const Overlay = styled('div')`
  141. grid-row: 1;
  142. grid-column: 3;
  143. height: 100%;
  144. width: 100%;
  145. position: absolute;
  146. pointer-events: none;
  147. `;
  148. const GridLineContainer = styled('div')`
  149. margin-left: -1px;
  150. position: relative;
  151. height: 100%;
  152. z-index: 1;
  153. `;
  154. const LabelsContainer = styled('div')`
  155. position: relative;
  156. align-self: stretch;
  157. `;
  158. const Gridline = styled('div')<{left: number}>`
  159. position: absolute;
  160. left: ${p => p.left}px;
  161. border-left: 1px solid ${p => p.theme.translucentInnerBorder};
  162. height: 100%;
  163. `;
  164. const TimeLabelContainer = styled(Gridline)`
  165. display: flex;
  166. height: 100%;
  167. align-items: center;
  168. border-left: none;
  169. `;
  170. const TimeLabel = styled(DateTime)`
  171. font-variant-numeric: tabular-nums;
  172. font-size: ${p => p.theme.fontSizeSmall};
  173. color: ${p => p.theme.subText};
  174. margin-left: ${space(1)};
  175. `;