serviceIncidents.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import {Fragment, useCallback} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import Color from 'color';
  5. import moment, {type Moment} from 'moment-timezone';
  6. import Alert from 'sentry/components/alert';
  7. import {Hovercard} from 'sentry/components/hovercard';
  8. import {ServiceIncidentDetails} from 'sentry/components/serviceIncidentDetails';
  9. import {IconExclamation} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {StatusPageComponent, type StatuspageIncident} from 'sentry/types/system';
  12. import {useServiceIncidents} from 'sentry/utils/useServiceIncidents';
  13. import type {TimeWindowConfig} from './types';
  14. interface CronServiceIncidentsProps {
  15. timeWindowConfig: TimeWindowConfig;
  16. }
  17. interface GetTimeWindowConfigOptions {
  18. inc: StatuspageIncident;
  19. /**
  20. * Clamp the incident start and end times to be the timeWindowConfig start
  21. * and end
  22. */
  23. clamp?: boolean;
  24. }
  25. function CronServiceIncidents({timeWindowConfig}: CronServiceIncidentsProps) {
  26. const {data: incidents} = useServiceIncidents({
  27. // TODO(epurkhiser): There is also the EU region. We should make sure we
  28. // filter down to that region as well
  29. componentFilter: [StatusPageComponent.US_CRON_MONITORING],
  30. includeResolved: true,
  31. });
  32. const getIncidentTimes = useCallback(
  33. ({inc, clamp}: GetTimeWindowConfigOptions) => {
  34. const start = inc.started_at ? moment(inc.started_at) : moment(inc.created_at);
  35. const end = inc.resolved_at
  36. ? moment(inc.resolved_at)
  37. : moment(timeWindowConfig.end);
  38. if (!clamp) {
  39. return {start, end};
  40. }
  41. return {
  42. start: moment.max(start, moment(timeWindowConfig.start)),
  43. end: moment.min(end, moment(timeWindowConfig.end)),
  44. };
  45. },
  46. [timeWindowConfig]
  47. );
  48. const getPositionFromTime = useCallback(
  49. (time: Moment) => {
  50. const {start, elapsedMinutes, timelineWidth} = timeWindowConfig;
  51. const msPerPixel = (elapsedMinutes * 60 * 1000) / timelineWidth;
  52. return (time.valueOf() - start.getTime()) / msPerPixel;
  53. },
  54. [timeWindowConfig]
  55. );
  56. const incidentsInWindow = incidents?.filter(inc => {
  57. const {start: windowStart, end: windowEnd} = timeWindowConfig;
  58. const {start, end} = getIncidentTimes({inc});
  59. const startInWindow = start.isBetween(windowStart, windowEnd, undefined, '[]');
  60. const endInWindow = end.isBetween(windowStart, windowEnd, undefined, '[]');
  61. const overlapsWindow = start.isBefore(windowStart) && end.isAfter(windowEnd);
  62. return startInWindow || endInWindow || overlapsWindow;
  63. });
  64. if (!incidentsInWindow) {
  65. return null;
  66. }
  67. return incidentsInWindow.map(inc => {
  68. const {start, end} = getIncidentTimes({inc, clamp: true});
  69. const position = css`
  70. --incidentOverlayStart: ${getPositionFromTime(start)}px;
  71. --incidentOverlayEnd: ${getPositionFromTime(end)}px;
  72. `;
  73. const alertMessage =
  74. inc.status === 'unresolved'
  75. ? t(
  76. 'Sentry is currently experiencing an outage which may affect Check-In reliability.'
  77. )
  78. : t(
  79. "Sentry experienced an outage which may have affected check-in's during this time."
  80. );
  81. return (
  82. <Fragment key={inc.id}>
  83. <IncidentHovercard
  84. skipWrapper
  85. body={
  86. <Fragment>
  87. <Alert type="warning">{alertMessage}</Alert>
  88. <ServiceIncidentDetails incident={inc} />
  89. </Fragment>
  90. }
  91. >
  92. <IncidentIndicator css={position}>
  93. <IconExclamation color="white" />
  94. </IncidentIndicator>
  95. </IncidentHovercard>
  96. <IncidentOverlay css={position} />
  97. </Fragment>
  98. );
  99. });
  100. }
  101. const IncidentHovercard = styled(Hovercard)`
  102. width: 400px;
  103. max-height: 500px;
  104. overflow-y: scroll;
  105. `;
  106. const IncidentOverlay = styled('div')`
  107. position: absolute;
  108. top: 50px;
  109. height: 100%;
  110. height: calc(100% - 50px);
  111. left: var(--incidentOverlayStart);
  112. width: calc(var(--incidentOverlayEnd) - var(--incidentOverlayStart));
  113. pointer-events: none;
  114. background: ${p => Color(p.theme.yellow100).alpha(0.05).toString()};
  115. border-left: 1px solid ${p => p.theme.yellow200};
  116. border-right: 1px solid ${p => p.theme.yellow200};
  117. z-index: 2;
  118. `;
  119. const IncidentIndicator = styled('div')`
  120. position: absolute;
  121. top: 50px;
  122. left: var(--incidentOverlayStart);
  123. width: calc(var(--incidentOverlayEnd) - var(--incidentOverlayStart));
  124. transform: translateY(-50%);
  125. display: flex;
  126. align-items: center;
  127. z-index: 2;
  128. height: 20px;
  129. > svg,
  130. &:before {
  131. background: ${p => p.theme.yellow300};
  132. }
  133. > svg {
  134. position: absolute;
  135. left: 50%;
  136. transform: translateX(-50%);
  137. border-radius: 50%;
  138. height: 16px;
  139. width: 16px;
  140. padding: 3px;
  141. }
  142. &:before {
  143. content: '';
  144. display: block;
  145. height: 3px;
  146. width: inherit;
  147. }
  148. `;
  149. export {CronServiceIncidents};