mockTimelineVisualization.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import {Fragment, useContext, useEffect, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import FormContext from 'sentry/components/forms/formContext';
  4. import type {FieldValue} from 'sentry/components/forms/model';
  5. import Panel from 'sentry/components/panels/panel';
  6. import Placeholder from 'sentry/components/placeholder';
  7. import {t} from 'sentry/locale';
  8. import {useApiQuery} from 'sentry/utils/queryClient';
  9. import {useDimensions} from 'sentry/utils/useDimensions';
  10. import useOrganization from 'sentry/utils/useOrganization';
  11. import {ScheduleType} from 'sentry/views/monitors/types';
  12. import {CheckInPlaceholder} from './timeline/checkInPlaceholder';
  13. import {MockCheckInTimeline} from './timeline/checkInTimeline';
  14. import {GridLineLabels, GridLineOverlay} from './timeline/gridLines';
  15. import {getConfigFromTimeRange} from './timeline/utils/getConfigFromTimeRange';
  16. interface ScheduleConfig {
  17. cronSchedule?: FieldValue;
  18. intervalFrequency?: FieldValue;
  19. intervalUnit?: FieldValue;
  20. scheduleType?: FieldValue;
  21. timezone?: FieldValue;
  22. }
  23. const NUM_SAMPLE_TICKS = 9;
  24. function isValidConfig(schedule: ScheduleConfig) {
  25. const {scheduleType, cronSchedule, intervalFrequency, intervalUnit} = schedule;
  26. return !!(
  27. (scheduleType === ScheduleType.CRONTAB && cronSchedule) ||
  28. (scheduleType === ScheduleType.INTERVAL && intervalFrequency && intervalUnit)
  29. );
  30. }
  31. interface Props {
  32. schedule: ScheduleConfig;
  33. }
  34. export function MockTimelineVisualization({schedule}: Props) {
  35. const {scheduleType, cronSchedule, timezone, intervalFrequency, intervalUnit} =
  36. schedule;
  37. const organization = useOrganization();
  38. const {form} = useContext(FormContext);
  39. const query = {
  40. num_ticks: NUM_SAMPLE_TICKS,
  41. schedule_type: scheduleType,
  42. timezone,
  43. schedule:
  44. scheduleType === 'interval' ? [intervalFrequency, intervalUnit] : cronSchedule,
  45. };
  46. const elementRef = useRef<HTMLDivElement>(null);
  47. const {width: timelineWidth} = useDimensions<HTMLDivElement>({elementRef});
  48. const sampleDataQueryKey = [
  49. `/organizations/${organization.slug}/monitors-schedule-data/`,
  50. {query},
  51. ] as const;
  52. const {data, isPending, isError, error} = useApiQuery<number[]>(sampleDataQueryKey, {
  53. staleTime: 0,
  54. enabled: isValidConfig(schedule),
  55. retry: false,
  56. });
  57. const errorMessage =
  58. isError || !isValidConfig(schedule)
  59. ? error?.responseJSON?.schedule?.[0] ?? t('Invalid Schedule')
  60. : null;
  61. useEffect(() => {
  62. if (!form) {
  63. return;
  64. }
  65. if (scheduleType === ScheduleType.INTERVAL) {
  66. form.setError('config.schedule.frequency', errorMessage);
  67. } else if (scheduleType === ScheduleType.CRONTAB) {
  68. form.setError('config.schedule', errorMessage);
  69. }
  70. }, [errorMessage, form, scheduleType]);
  71. const mockTimestamps = data?.map(ts => new Date(ts * 1000));
  72. const start = mockTimestamps?.[0];
  73. const end = mockTimestamps?.[mockTimestamps.length - 1];
  74. const timeWindowConfig =
  75. start && end ? getConfigFromTimeRange(start, end, timelineWidth) : undefined;
  76. return (
  77. <TimelineContainer>
  78. <TimelineWidthTracker ref={elementRef} />
  79. {isPending || !start || !end || !timeWindowConfig || !mockTimestamps ? (
  80. <Fragment>
  81. <Placeholder height="50px" />
  82. {errorMessage ? null : <CheckInPlaceholder />}
  83. </Fragment>
  84. ) : (
  85. <Fragment>
  86. <AlignedGridLineLabels timeWindowConfig={timeWindowConfig} />
  87. <AlignedGridLineOverlay
  88. showCursor={!isPending}
  89. timeWindowConfig={timeWindowConfig}
  90. />
  91. <MockCheckInTimeline
  92. mockTimestamps={mockTimestamps.slice(1, mockTimestamps.length - 1)}
  93. timeWindowConfig={timeWindowConfig}
  94. />
  95. </Fragment>
  96. )}
  97. </TimelineContainer>
  98. );
  99. }
  100. const TimelineContainer = styled(Panel)`
  101. display: grid;
  102. grid-template-columns: 1fr;
  103. grid-template-rows: auto 60px;
  104. align-items: center;
  105. `;
  106. const AlignedGridLineLabels = styled(GridLineLabels)`
  107. grid-column: 0;
  108. border-bottom: 1px solid ${p => p.theme.border};
  109. `;
  110. const AlignedGridLineOverlay = styled(GridLineOverlay)`
  111. grid-column: 0;
  112. `;
  113. const TimelineWidthTracker = styled('div')`
  114. position: absolute;
  115. width: 100%;
  116. grid-row: 1;
  117. grid-column: 0;
  118. `;