import {Fragment, useCallback} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import Color from 'color';
import moment, {type Moment} from 'moment-timezone';
import Alert from 'sentry/components/alert';
import {Hovercard} from 'sentry/components/hovercard';
import {ServiceIncidentDetails} from 'sentry/components/serviceIncidentDetails';
import {IconExclamation} from 'sentry/icons';
import {t} from 'sentry/locale';
import {StatusPageComponent, type StatuspageIncident} from 'sentry/types/system';
import {useServiceIncidents} from 'sentry/utils/useServiceIncidents';
import type {TimeWindowConfig} from './types';
interface CronServiceIncidentsProps {
timeWindowConfig: TimeWindowConfig;
}
interface GetTimeWindowConfigOptions {
inc: StatuspageIncident;
/**
* Clamp the incident start and end times to be the timeWindowConfig start
* and end
*/
clamp?: boolean;
}
function CronServiceIncidents({timeWindowConfig}: CronServiceIncidentsProps) {
const {data: incidents} = useServiceIncidents({
// TODO(epurkhiser): There is also the EU region. We should make sure we
// filter down to that region as well
componentFilter: [StatusPageComponent.US_CRON_MONITORING],
includeResolved: true,
});
const getIncidentTimes = useCallback(
({inc, clamp}: GetTimeWindowConfigOptions) => {
const start = inc.started_at ? moment(inc.started_at) : moment(inc.created_at);
const end = inc.resolved_at
? moment(inc.resolved_at)
: moment(timeWindowConfig.end);
if (!clamp) {
return {start, end};
}
return {
start: moment.max(start, moment(timeWindowConfig.start)),
end: moment.min(end, moment(timeWindowConfig.end)),
};
},
[timeWindowConfig]
);
const getPositionFromTime = useCallback(
(time: Moment) => {
const {start, elapsedMinutes, timelineWidth} = timeWindowConfig;
const msPerPixel = (elapsedMinutes * 60 * 1000) / timelineWidth;
return (time.valueOf() - start.getTime()) / msPerPixel;
},
[timeWindowConfig]
);
const incidentsInWindow = incidents?.filter(inc => {
const {start: windowStart, end: windowEnd} = timeWindowConfig;
const {start, end} = getIncidentTimes({inc});
const startInWindow = start.isBetween(windowStart, windowEnd, undefined, '[]');
const endInWindow = end.isBetween(windowStart, windowEnd, undefined, '[]');
const overlapsWindow = start.isBefore(windowStart) && end.isAfter(windowEnd);
return startInWindow || endInWindow || overlapsWindow;
});
if (!incidentsInWindow) {
return null;
}
return incidentsInWindow.map(inc => {
const {start, end} = getIncidentTimes({inc, clamp: true});
const position = css`
--incidentOverlayStart: ${getPositionFromTime(start)}px;
--incidentOverlayEnd: ${getPositionFromTime(end)}px;
`;
const alertMessage =
inc.status === 'unresolved'
? t(
'Sentry is currently experiencing an outage which may affect Check-In reliability.'
)
: t(
"Sentry experienced an outage which may have affected check-in's during this time."
);
return (
{alertMessage}
}
>
);
});
}
const IncidentHovercard = styled(Hovercard)`
width: 400px;
max-height: 500px;
overflow-y: scroll;
`;
const IncidentOverlay = styled('div')`
position: absolute;
top: 50px;
height: 100%;
height: calc(100% - 50px);
left: var(--incidentOverlayStart);
width: calc(var(--incidentOverlayEnd) - var(--incidentOverlayStart));
pointer-events: none;
background: ${p => Color(p.theme.yellow100).alpha(0.05).toString()};
border-left: 1px solid ${p => p.theme.yellow200};
border-right: 1px solid ${p => p.theme.yellow200};
z-index: 2;
`;
const IncidentIndicator = styled('div')`
position: absolute;
top: 50px;
left: var(--incidentOverlayStart);
width: calc(var(--incidentOverlayEnd) - var(--incidentOverlayStart));
transform: translateY(-50%);
display: flex;
align-items: center;
z-index: 2;
height: 20px;
> svg,
&:before {
background: ${p => p.theme.yellow300};
}
> svg {
position: absolute;
left: 50%;
transform: translateX(-50%);
border-radius: 50%;
height: 16px;
width: 16px;
padding: 3px;
}
&:before {
content: '';
display: block;
height: 3px;
width: inherit;
}
`;
export {CronServiceIncidents};