123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- import {useCallback} from 'react';
- import styled from '@emotion/styled';
- import {mergeRefs} from '@react-aria/utils';
- import moment from 'moment';
- import {updateDateTime} from 'sentry/actionCreators/pageFilters';
- import DateTime from 'sentry/components/dateTime';
- import {space} from 'sentry/styles/space';
- import useRouter from 'sentry/utils/useRouter';
- import type {TimeWindowConfig} from 'sentry/views/monitors/components/overviewTimeline/types';
- import {useTimelineCursor} from './timelineCursor';
- import {useTimelineZoom} from './timelineZoom';
- interface Props {
- timeWindowConfig: TimeWindowConfig;
- /**
- * The size of the timeline
- */
- width: number;
- /**
- * Enable zoom selection
- */
- allowZoom?: boolean;
- className?: string;
- /**
- * Enable the timeline cursor
- */
- showCursor?: boolean;
- /**
- * Enabling causes the cursor tooltip to stick to the top of the viewport.
- */
- stickyCursor?: boolean;
- }
- /**
- * Aligns a date to a clean offset such as start of minute, hour, day
- * based on the interval of how far each time label is placed.
- */
- function alignTimeMarkersToStartOf(date: moment.Moment, timeMarkerInterval: number) {
- if (timeMarkerInterval < 60) {
- date.minute(date.minutes() - (date.minutes() % timeMarkerInterval));
- } else if (timeMarkerInterval < 60 * 24) {
- date.startOf('hour');
- } else {
- date.startOf('day');
- }
- }
- interface TimeMarker {
- date: Date;
- position: number;
- }
- function getTimeMarkersFromConfig(config: TimeWindowConfig, width: number) {
- const {start, end, elapsedMinutes, timeMarkerInterval} = config;
- const msPerPixel = (elapsedMinutes * 60 * 1000) / width;
- const times: TimeMarker[] = [];
- const lastTimeMark = moment(end);
- alignTimeMarkersToStartOf(lastTimeMark, timeMarkerInterval);
- // Generate time markers which represent location of grid lines/time labels
- for (let i = 1; i < elapsedMinutes / timeMarkerInterval; i++) {
- const timeMark = moment(lastTimeMark).subtract(i * timeMarkerInterval, 'minute');
- const position = (timeMark.valueOf() - start.valueOf()) / msPerPixel;
- times.push({date: timeMark.toDate(), position});
- }
- return times.reverse();
- }
- export function GridLineTimeLabels({width, timeWindowConfig, className}: Props) {
- return (
- <LabelsContainer className={className}>
- {getTimeMarkersFromConfig(timeWindowConfig, width).map(({date, position}) => (
- <TimeLabelContainer key={date.getTime()} left={position}>
- <TimeLabel date={date} {...timeWindowConfig.dateTimeProps} />
- </TimeLabelContainer>
- ))}
- </LabelsContainer>
- );
- }
- export function GridLineOverlay({
- width,
- timeWindowConfig,
- showCursor,
- stickyCursor,
- allowZoom,
- className,
- }: Props) {
- const router = useRouter();
- const {start, dateLabelFormat} = timeWindowConfig;
- const msPerPixel = (timeWindowConfig.elapsedMinutes * 60 * 1000) / width;
- const dateFromPosition = useCallback(
- (position: number) => moment(start.getTime() + msPerPixel * position),
- [msPerPixel, start]
- );
- const makeCursorLabel = useCallback(
- (position: number) => dateFromPosition(position).format(dateLabelFormat),
- [dateFromPosition, dateLabelFormat]
- );
- const handleZoom = useCallback(
- (startX: number, endX: number) =>
- updateDateTime(
- {
- start: dateFromPosition(startX).toDate(),
- end: dateFromPosition(endX).toDate(),
- },
- router
- ),
- [dateFromPosition, router]
- );
- const {
- selectionContainerRef,
- timelineSelector,
- isActive: selectionIsActive,
- } = useTimelineZoom<HTMLDivElement>({enabled: !!allowZoom, onSelect: handleZoom});
- const {cursorContainerRef, timelineCursor} = useTimelineCursor<HTMLDivElement>({
- enabled: showCursor && !selectionIsActive,
- sticky: stickyCursor,
- labelText: makeCursorLabel,
- });
- const overlayRef = mergeRefs(cursorContainerRef, selectionContainerRef);
- return (
- <Overlay ref={overlayRef} className={className}>
- {timelineCursor}
- {timelineSelector}
- <GridLineContainer>
- {getTimeMarkersFromConfig(timeWindowConfig, width).map(({date, position}) => (
- <Gridline key={date.getTime()} left={position} />
- ))}
- </GridLineContainer>
- </Overlay>
- );
- }
- const Overlay = styled('div')`
- grid-row: 1;
- grid-column: 3;
- height: 100%;
- width: 100%;
- position: absolute;
- pointer-events: none;
- `;
- const GridLineContainer = styled('div')`
- position: relative;
- height: 100%;
- z-index: 1;
- `;
- const LabelsContainer = styled('div')`
- position: relative;
- align-self: stretch;
- `;
- const Gridline = styled('div')<{left: number}>`
- position: absolute;
- left: ${p => p.left}px;
- border-left: 1px solid ${p => p.theme.translucentInnerBorder};
- height: 100%;
- `;
- const TimeLabelContainer = styled(Gridline)`
- display: flex;
- height: 100%;
- align-items: center;
- border-left: none;
- `;
- const TimeLabel = styled(DateTime)`
- font-variant-numeric: tabular-nums;
- font-size: ${p => p.theme.fontSizeSmall};
- color: ${p => p.theme.subText};
- margin-left: ${space(1)};
- `;
|