mockTimelineVisualization.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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 {MockCheckInTimeline} from 'sentry/views/monitors/components/overviewTimeline/checkInTimeline';
  12. import {
  13. GridLineOverlay,
  14. GridLineTimeLabels,
  15. } from 'sentry/views/monitors/components/overviewTimeline/gridLines';
  16. import {TimelinePlaceholder} from 'sentry/views/monitors/components/overviewTimeline/timelinePlaceholder';
  17. import {getConfigFromTimeRange} from 'sentry/views/monitors/components/overviewTimeline/utils';
  18. import {ScheduleType} from 'sentry/views/monitors/types';
  19. interface ScheduleConfig {
  20. cronSchedule?: FieldValue;
  21. intervalFrequency?: FieldValue;
  22. intervalUnit?: FieldValue;
  23. scheduleType?: FieldValue;
  24. }
  25. const NUM_SAMPLE_TICKS = 9;
  26. function isValidConfig(schedule: ScheduleConfig) {
  27. const {scheduleType, cronSchedule, intervalFrequency, intervalUnit} = schedule;
  28. return !!(
  29. (scheduleType === ScheduleType.CRONTAB && cronSchedule) ||
  30. (scheduleType === ScheduleType.INTERVAL && intervalFrequency && intervalUnit)
  31. );
  32. }
  33. interface Props {
  34. schedule: ScheduleConfig;
  35. }
  36. export function MockTimelineVisualization({schedule}: Props) {
  37. const {scheduleType, cronSchedule, intervalFrequency, intervalUnit} = schedule;
  38. const organization = useOrganization();
  39. const {form} = useContext(FormContext);
  40. const query = {
  41. num_ticks: NUM_SAMPLE_TICKS,
  42. schedule_type: scheduleType,
  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, isLoading, 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. {isLoading || !start || !end || !timeWindowConfig || !mockTimestamps ? (
  80. <Fragment>
  81. <Placeholder height="40px" />
  82. {errorMessage ? (
  83. <Placeholder testId="error-placeholder" height="100px" />
  84. ) : (
  85. <TimelinePlaceholder />
  86. )}
  87. </Fragment>
  88. ) : (
  89. <Fragment>
  90. <StyledGridLineTimeLabels
  91. timeWindowConfig={timeWindowConfig}
  92. start={start}
  93. end={end}
  94. width={timelineWidth}
  95. />
  96. <StyledGridLineOverlay
  97. showCursor={!isLoading}
  98. timeWindowConfig={timeWindowConfig}
  99. start={start}
  100. end={end}
  101. width={timelineWidth}
  102. />
  103. <MockCheckInTimeline
  104. width={timelineWidth}
  105. mockTimestamps={mockTimestamps.slice(1, mockTimestamps.length - 1)}
  106. start={start}
  107. end={end}
  108. timeWindowConfig={timeWindowConfig}
  109. />
  110. </Fragment>
  111. )}
  112. </TimelineContainer>
  113. );
  114. }
  115. const TimelineContainer = styled(Panel)`
  116. display: grid;
  117. grid-template-columns: 1fr;
  118. grid-template-rows: 40px 100px;
  119. align-items: center;
  120. `;
  121. const StyledGridLineTimeLabels = styled(GridLineTimeLabels)`
  122. grid-column: 0;
  123. border-bottom: 1px solid ${p => p.theme.border};
  124. `;
  125. const StyledGridLineOverlay = styled(GridLineOverlay)`
  126. grid-column: 0;
  127. `;
  128. const TimelineWidthTracker = styled('div')`
  129. position: absolute;
  130. width: 100%;
  131. grid-row: 1;
  132. grid-column: 0;
  133. `;