issueUptimeCheckTimeline.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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. const statuses = [
  75. CheckStatus.SUCCESS,
  76. CheckStatus.FAILURE,
  77. CheckStatus.FAILURE_INCIDENT,
  78. ];
  79. if (hasUnknownStatus) {
  80. statuses.push(CheckStatus.MISSED_WINDOW);
  81. }
  82. return statuses;
  83. }, [uptimeAlertId, uptimeStats]);
  84. return (
  85. <ChartContainer>
  86. <TimelineLegend ref={elementRef} role="caption">
  87. {legendStatuses.map(status => (
  88. <Flex align="center" gap={space(0.5)} key={status}>
  89. <CheckIndicator status={status} width={8} />
  90. <TimelineLegendText>{statusToText[status]}</TimelineLegendText>
  91. </Flex>
  92. ))}
  93. </TimelineLegend>
  94. <GridLineOverlay
  95. allowZoom
  96. showCursor
  97. timeWindowConfig={timeWindowConfig}
  98. labelPosition="center-bottom"
  99. />
  100. <GridLineLabels timeWindowConfig={timeWindowConfig} labelPosition="center-bottom" />
  101. <TimelineContainer>
  102. {isPending ? (
  103. <CheckInPlaceholder />
  104. ) : (
  105. <CheckInTimeline
  106. bucketedData={uptimeAlertId ? (uptimeStats?.[uptimeAlertId] ?? []) : []}
  107. statusLabel={statusToText}
  108. statusStyle={tickStyle}
  109. statusPrecedent={checkStatusPrecedent}
  110. timeWindowConfig={timeWindowConfig}
  111. />
  112. )}
  113. </TimelineContainer>
  114. </ChartContainer>
  115. );
  116. }
  117. const ChartContainer = styled('div')`
  118. position: relative;
  119. min-height: 100px;
  120. width: 100%;
  121. `;
  122. const TimelineLegend = styled('div')`
  123. position: absolute;
  124. width: 100%;
  125. user-select: none;
  126. display: flex;
  127. gap: ${space(1)};
  128. margin-top: ${space(1.5)};
  129. `;
  130. const TimelineLegendText = styled('div')`
  131. color: ${p => p.theme.subText};
  132. font-size: ${p => p.theme.fontSizeSmall};
  133. `;
  134. const TimelineContainer = styled('div')`
  135. position: absolute;
  136. top: 36px;
  137. width: 100%;
  138. `;