gridLines.tsx 4.8 KB

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