issueUptimeCheckTimeline.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import {useMemo, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import {CheckInPlaceholder} from 'sentry/components/checkInTimeline/checkInPlaceholder';
  4. import {CheckInTimeline} from 'sentry/components/checkInTimeline/checkInTimeline';
  5. import {
  6. GridLineLabels,
  7. GridLineOverlay,
  8. } from 'sentry/components/checkInTimeline/gridLines';
  9. import {useTimeWindowConfig} from 'sentry/components/checkInTimeline/hooks/useTimeWindowConfig';
  10. import {Flex} from 'sentry/components/container/flex';
  11. import {space} from 'sentry/styles/space';
  12. import type {Event} from 'sentry/types/event';
  13. import type {Group} from 'sentry/types/group';
  14. import {useApiQuery} from 'sentry/utils/queryClient';
  15. import {useDebouncedValue} from 'sentry/utils/useDebouncedValue';
  16. import {useDimensions} from 'sentry/utils/useDimensions';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {useUser} from 'sentry/utils/useUser';
  19. import {CheckIndicator} from 'sentry/views/alerts/rules/uptime/checkIndicator';
  20. import {CheckStatus} from 'sentry/views/alerts/rules/uptime/types';
  21. import {
  22. checkStatusPrecedent,
  23. statusToText,
  24. tickStyle,
  25. } from 'sentry/views/insights/uptime/timelineConfig';
  26. import {useUptimeMonitorStats} from 'sentry/views/insights/uptime/utils/useUptimeMonitorStats';
  27. import {useIssueDetails} from 'sentry/views/issueDetails/streamline/context';
  28. import {getGroupEventQueryKey} from 'sentry/views/issueDetails/utils';
  29. export function useUptimeIssueAlertId({groupId}: {groupId: string}): string | undefined {
  30. /**
  31. * This should be removed once the uptime rule value is set on the issue.
  32. * This will fetch an event from the max range if the detector details
  33. * are not available (e.g. time range has changed and page refreshed)
  34. */
  35. const user = useUser();
  36. const organization = useOrganization();
  37. const {detectorDetails} = useIssueDetails();
  38. const {detectorId, detectorType} = detectorDetails;
  39. const hasUptimeDetector = detectorId && detectorType === 'uptime_monitor';
  40. const {data: event} = useApiQuery<Event>(
  41. getGroupEventQueryKey({
  42. orgSlug: organization.slug,
  43. groupId,
  44. eventId: user.options.defaultIssueEvent,
  45. environments: [],
  46. }),
  47. {
  48. staleTime: Infinity,
  49. enabled: !hasUptimeDetector,
  50. retry: false,
  51. }
  52. );
  53. // Fall back to the fetched event since the legacy UI isn't nested within the provider the provider
  54. return hasUptimeDetector
  55. ? detectorId
  56. : event?.tags?.find(tag => tag.key === 'uptime_rule')?.value;
  57. }
  58. export function IssueUptimeCheckTimeline({group}: {group: Group}) {
  59. const uptimeAlertId = useUptimeIssueAlertId({groupId: group.id});
  60. const elementRef = useRef<HTMLDivElement>(null);
  61. const {width: containerWidth} = useDimensions<HTMLDivElement>({elementRef});
  62. const timelineWidth = useDebouncedValue(containerWidth, 500);
  63. const timeWindowConfig = useTimeWindowConfig({timelineWidth});
  64. const {data: uptimeStats, isPending} = useUptimeMonitorStats({
  65. ruleIds: uptimeAlertId ? [uptimeAlertId] : [],
  66. timeWindowConfig,
  67. });
  68. const legendStatuses = useMemo(() => {
  69. const hasUnknownStatus =
  70. uptimeAlertId &&
  71. uptimeStats?.[uptimeAlertId]?.some(
  72. ([_, stats]) => stats[CheckStatus.MISSED_WINDOW] > 0
  73. );
  74. return [CheckStatus.SUCCESS, CheckStatus.MISSED_WINDOW, CheckStatus.FAILURE].filter(
  75. status => (hasUnknownStatus ? true : status !== CheckStatus.MISSED_WINDOW)
  76. );
  77. }, [uptimeAlertId, uptimeStats]);
  78. return (
  79. <ChartContainer>
  80. <TimelineLegend ref={elementRef} role="caption">
  81. {legendStatuses.map(status => (
  82. <Flex align="center" gap={space(0.5)} key={status}>
  83. <CheckIndicator status={status} width={8} />
  84. <TimelineLegendText>{statusToText[status]}</TimelineLegendText>
  85. </Flex>
  86. ))}
  87. </TimelineLegend>
  88. <GridLineOverlay
  89. stickyCursor
  90. allowZoom
  91. showCursor
  92. timeWindowConfig={timeWindowConfig}
  93. labelPosition="center-bottom"
  94. />
  95. <GridLineLabels timeWindowConfig={timeWindowConfig} labelPosition="center-bottom" />
  96. <TimelineContainer>
  97. {isPending ? (
  98. <CheckInPlaceholder />
  99. ) : (
  100. <CheckInTimeline
  101. bucketedData={uptimeAlertId ? uptimeStats?.[uptimeAlertId] ?? [] : []}
  102. statusLabel={statusToText}
  103. statusStyle={tickStyle}
  104. statusPrecedent={checkStatusPrecedent}
  105. timeWindowConfig={timeWindowConfig}
  106. />
  107. )}
  108. </TimelineContainer>
  109. </ChartContainer>
  110. );
  111. }
  112. const ChartContainer = styled('div')`
  113. position: relative;
  114. min-height: 104px;
  115. width: 100%;
  116. `;
  117. const TimelineLegend = styled('div')`
  118. position: absolute;
  119. width: 100%;
  120. user-select: none;
  121. display: flex;
  122. gap: ${space(1)};
  123. margin-top: ${space(1.5)};
  124. `;
  125. const TimelineLegendText = styled('div')`
  126. color: ${p => p.theme.subText};
  127. font-size: ${p => p.theme.fontSizeSmall};
  128. `;
  129. const TimelineContainer = styled('div')`
  130. position: absolute;
  131. top: 36px;
  132. `;