gridLines.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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. interface Props {
  13. timeWindowConfig: TimeWindowConfig;
  14. /**
  15. * The size of the timeline
  16. */
  17. width: number;
  18. /**
  19. * Enable zoom selection
  20. */
  21. allowZoom?: boolean;
  22. className?: string;
  23. /**
  24. * Enable the timeline cursor
  25. */
  26. showCursor?: boolean;
  27. /**
  28. * Enabling causes the cursor tooltip to stick to the top of the viewport.
  29. */
  30. stickyCursor?: boolean;
  31. }
  32. /**
  33. * Aligns a date to a clean offset such as start of minute, hour, day
  34. * based on the interval of how far each time label is placed.
  35. */
  36. function alignTimeMarkersToStartOf(date: moment.Moment, timeMarkerInterval: number) {
  37. if (timeMarkerInterval < 60) {
  38. date.minute(date.minutes() - (date.minutes() % timeMarkerInterval));
  39. } else if (timeMarkerInterval < 60 * 24) {
  40. date.startOf('hour');
  41. } else {
  42. date.startOf('day');
  43. }
  44. }
  45. interface TimeMarker {
  46. date: Date;
  47. position: number;
  48. }
  49. function getTimeMarkersFromConfig(config: TimeWindowConfig, width: number) {
  50. const {start, end, elapsedMinutes, timeMarkerInterval} = config;
  51. const msPerPixel = (elapsedMinutes * 60 * 1000) / width;
  52. const times: TimeMarker[] = [];
  53. const lastTimeMark = moment(end);
  54. alignTimeMarkersToStartOf(lastTimeMark, timeMarkerInterval);
  55. // Generate time markers which represent location of grid lines/time labels
  56. for (let i = 1; i < elapsedMinutes / timeMarkerInterval; i++) {
  57. const timeMark = moment(lastTimeMark).subtract(i * timeMarkerInterval, 'minute');
  58. const position = (timeMark.valueOf() - start.valueOf()) / msPerPixel;
  59. times.push({date: timeMark.toDate(), position});
  60. }
  61. return times.reverse();
  62. }
  63. export function GridLineTimeLabels({width, timeWindowConfig, className}: Props) {
  64. return (
  65. <LabelsContainer className={className}>
  66. {getTimeMarkersFromConfig(timeWindowConfig, width).map(({date, position}) => (
  67. <TimeLabelContainer key={date.getTime()} left={position}>
  68. <TimeLabel date={date} {...timeWindowConfig.dateTimeProps} />
  69. </TimeLabelContainer>
  70. ))}
  71. </LabelsContainer>
  72. );
  73. }
  74. export function GridLineOverlay({
  75. width,
  76. timeWindowConfig,
  77. showCursor,
  78. stickyCursor,
  79. allowZoom,
  80. className,
  81. }: Props) {
  82. const router = useRouter();
  83. const {start, dateLabelFormat} = timeWindowConfig;
  84. const msPerPixel = (timeWindowConfig.elapsedMinutes * 60 * 1000) / width;
  85. const dateFromPosition = useCallback(
  86. (position: number) => moment(start.getTime() + msPerPixel * position),
  87. [msPerPixel, start]
  88. );
  89. const makeCursorLabel = useCallback(
  90. (position: number) => dateFromPosition(position).format(dateLabelFormat),
  91. [dateFromPosition, dateLabelFormat]
  92. );
  93. const handleZoom = useCallback(
  94. (startX: number, endX: number) =>
  95. updateDateTime(
  96. {
  97. start: dateFromPosition(startX).toDate(),
  98. end: dateFromPosition(endX).toDate(),
  99. },
  100. router
  101. ),
  102. [dateFromPosition, router]
  103. );
  104. const {
  105. selectionContainerRef,
  106. timelineSelector,
  107. isActive: selectionIsActive,
  108. } = useTimelineZoom<HTMLDivElement>({enabled: !!allowZoom, onSelect: handleZoom});
  109. const {cursorContainerRef, timelineCursor} = useTimelineCursor<HTMLDivElement>({
  110. enabled: showCursor && !selectionIsActive,
  111. sticky: stickyCursor,
  112. labelText: makeCursorLabel,
  113. });
  114. const overlayRef = mergeRefs(cursorContainerRef, selectionContainerRef);
  115. return (
  116. <Overlay ref={overlayRef} className={className}>
  117. {timelineCursor}
  118. {timelineSelector}
  119. <GridLineContainer>
  120. {getTimeMarkersFromConfig(timeWindowConfig, width).map(({date, position}) => (
  121. <Gridline key={date.getTime()} left={position} />
  122. ))}
  123. </GridLineContainer>
  124. </Overlay>
  125. );
  126. }
  127. const Overlay = styled('div')`
  128. grid-row: 1;
  129. grid-column: 3;
  130. height: 100%;
  131. width: 100%;
  132. position: absolute;
  133. pointer-events: none;
  134. `;
  135. const GridLineContainer = styled('div')`
  136. position: relative;
  137. height: 100%;
  138. z-index: 1;
  139. `;
  140. const LabelsContainer = styled('div')`
  141. position: relative;
  142. align-self: stretch;
  143. `;
  144. const Gridline = styled('div')<{left: number}>`
  145. position: absolute;
  146. left: ${p => p.left}px;
  147. border-left: 1px solid ${p => p.theme.translucentInnerBorder};
  148. height: 100%;
  149. `;
  150. const TimeLabelContainer = styled(Gridline)`
  151. display: flex;
  152. height: 100%;
  153. align-items: center;
  154. border-left: none;
  155. `;
  156. const TimeLabel = styled(DateTime)`
  157. font-variant-numeric: tabular-nums;
  158. font-size: ${p => p.theme.fontSizeSmall};
  159. color: ${p => p.theme.subText};
  160. margin-left: ${space(1)};
  161. `;