serviceIncidents.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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';
  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';
  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. });
  31. const getIncidentTimes = useCallback(
  32. ({inc, clamp}: GetTimeWindowConfigOptions) => {
  33. const start = inc.started_at ? moment(inc.started_at) : moment(inc.created_at);
  34. const end = inc.resolved_at
  35. ? moment(inc.resolved_at)
  36. : moment(timeWindowConfig.end);
  37. if (!clamp) {
  38. return {start, end};
  39. }
  40. return {
  41. start: moment.max(start, moment(timeWindowConfig.start)),
  42. end: moment.min(end, moment(timeWindowConfig.end)),
  43. };
  44. },
  45. [timeWindowConfig]
  46. );
  47. const getPositionFromTime = useCallback(
  48. (time: Moment) => {
  49. const {start, elapsedMinutes, timelineWidth} = timeWindowConfig;
  50. const msPerPixel = (elapsedMinutes * 60 * 1000) / timelineWidth;
  51. return (time.valueOf() - start.getTime()) / msPerPixel;
  52. },
  53. [timeWindowConfig]
  54. );
  55. const incidentsInWindow = incidents?.filter(inc => {
  56. const {start: windowStart, end: windowEnd} = timeWindowConfig;
  57. const {start, end} = getIncidentTimes({inc});
  58. const startInWindow = start.isBetween(windowStart, windowEnd, undefined, '[]');
  59. const endInWindow = end.isBetween(windowStart, windowEnd, undefined, '[]');
  60. const overlapsWindow = start.isBefore(windowStart) && end.isAfter(windowEnd);
  61. return startInWindow || endInWindow || overlapsWindow;
  62. });
  63. if (!incidentsInWindow) {
  64. return null;
  65. }
  66. return incidentsInWindow.map(inc => {
  67. const {start, end} = getIncidentTimes({inc, clamp: true});
  68. const position = css`
  69. --incidentOverlayStart: ${getPositionFromTime(start)}px;
  70. --incidentOverlayEnd: ${getPositionFromTime(end)}px;
  71. `;
  72. const alertMessage =
  73. inc.status === 'unresolved'
  74. ? t(
  75. 'Sentry is currently experiencing an outage which may affect Check-In reliability.'
  76. )
  77. : t(
  78. "Sentry experienced an outage which may have affected check-in's during this time."
  79. );
  80. return (
  81. <Fragment key={inc.id}>
  82. <IncidentHovercard
  83. skipWrapper
  84. body={
  85. <Fragment>
  86. <Alert type="warning">{alertMessage}</Alert>
  87. <ServiceIncidentDetails incident={inc} />
  88. </Fragment>
  89. }
  90. >
  91. <IncidentIndicator css={position}>
  92. <IconExclamation color="white" />
  93. </IncidentIndicator>
  94. </IncidentHovercard>
  95. <IncidentOverlay css={position} />
  96. </Fragment>
  97. );
  98. });
  99. }
  100. const IncidentHovercard = styled(Hovercard)`
  101. width: 400px;
  102. `;
  103. const IncidentOverlay = styled('div')`
  104. position: absolute;
  105. top: 50px;
  106. height: 100%;
  107. height: calc(100% - 50px);
  108. left: var(--incidentOverlayStart);
  109. width: calc(var(--incidentOverlayEnd) - var(--incidentOverlayStart));
  110. pointer-events: none;
  111. background: ${p => Color(p.theme.yellow100).alpha(0.05).toString()};
  112. border-left: 1px solid ${p => p.theme.yellow200};
  113. border-right: 1px solid ${p => p.theme.yellow200};
  114. z-index: 2;
  115. `;
  116. const IncidentIndicator = styled('div')`
  117. position: absolute;
  118. top: 50px;
  119. left: var(--incidentOverlayStart);
  120. width: calc(var(--incidentOverlayEnd) - var(--incidentOverlayStart));
  121. transform: translateY(-50%);
  122. display: flex;
  123. align-items: center;
  124. z-index: 2;
  125. height: 20px;
  126. > svg,
  127. &:before {
  128. background: ${p => p.theme.yellow300};
  129. }
  130. > svg {
  131. position: absolute;
  132. left: 50%;
  133. transform: translateX(-50%);
  134. border-radius: 50%;
  135. height: 16px;
  136. width: 16px;
  137. padding: 3px;
  138. }
  139. &:before {
  140. content: '';
  141. display: block;
  142. height: 3px;
  143. width: inherit;
  144. }
  145. `;
  146. export {CronServiceIncidents};