mockTimelineVisualization.tsx 4.6 KB

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