Files
2024-02-15 17:01:50 +00:00

254 lines
6.8 KiB
TypeScript

import {Fragment, useEffect, useState} from 'react';
import styled from '@emotion/styled';
import * as Sentry from '@sentry/react';
import color from 'color';
import sortBy from 'lodash/sortBy';
import startCase from 'lodash/startCase';
import {loadIncidents} from 'sentry/actionCreators/serviceIncidents';
import {LinkButton} from 'sentry/components/button';
import List from 'sentry/components/list';
import ListItem from 'sentry/components/list/listItem';
import Text from 'sentry/components/text';
import TimeSince from 'sentry/components/timeSince';
import {Tooltip} from 'sentry/components/tooltip';
import {
IconCheckmark,
IconFatal,
IconFire,
IconInfo,
IconOpen,
IconWarning,
} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {SentryServiceStatus} from 'sentry/types';
import marked from 'sentry/utils/marked';
import SidebarItem from './sidebarItem';
import SidebarPanel from './sidebarPanel';
import SidebarPanelEmpty from './sidebarPanelEmpty';
import SidebarPanelItem from './sidebarPanelItem';
import type {CommonSidebarProps} from './types';
import {SidebarPanelKey} from './types';
type Props = CommonSidebarProps;
type Status =
SentryServiceStatus['incidents'][number]['affectedComponents'][number]['status'];
const COMPONENT_STATUS_SORT: Status[] = [
'operational',
'degraded_performance',
'partial_outage',
'major_outage',
];
function ServiceIncidents({
currentPanel,
onShowPanel,
hidePanel,
collapsed,
orientation,
}: Props) {
const [serviceStatus, setServiceStatus] = useState<SentryServiceStatus | null>(null);
async function fetchData() {
try {
setServiceStatus(await loadIncidents());
} catch (e) {
Sentry.withScope(scope => {
scope.setLevel('warning');
scope.setFingerprint(['ServiceIncidents-fetchData']);
Sentry.captureException(e);
});
}
}
useEffect(() => void fetchData(), []);
if (!serviceStatus) {
return null;
}
const active = currentPanel === SidebarPanelKey.SERVICE_INCIDENTS;
const isEmpty = !serviceStatus.incidents || serviceStatus.incidents.length === 0;
if (isEmpty) {
return null;
}
return (
<Fragment>
<SidebarItem
id="statusupdate"
orientation={orientation}
collapsed={collapsed}
active={active}
icon={<IconWarning size="md" />}
label={t('Service status')}
onClick={onShowPanel}
/>
{active && serviceStatus && (
<SidebarPanel
orientation={orientation}
title={t('Recent service updates')}
hidePanel={hidePanel}
collapsed={collapsed}
>
{isEmpty && (
<SidebarPanelEmpty>{t('There are no incidents to report')}</SidebarPanelEmpty>
)}
{serviceStatus.incidents.map(incident => (
<SidebarPanelItem title={incident.name} key={incident.id}>
<LinkButton
size="xs"
icon={<IconOpen />}
priority="link"
href={incident.url}
external
>
{t('Full Incident Details')}
</LinkButton>
<AffectedServices>
{tct(
"This incident started [timeAgo]. We're experiencing the following problems with our services",
{
timeAgo: (
<strong>
<TimeSince date={incident.createdAt} />
</strong>
),
}
)}
<ComponentList>
{sortBy(incident.affectedComponents, i =>
COMPONENT_STATUS_SORT.indexOf(i.status)
).map(({name, status}, key) => (
<ComponentStatus
key={key}
padding="24px"
symbol={getStatusSymbol(status)}
>
{name}
</ComponentStatus>
))}
</ComponentList>
</AffectedServices>
<UpdatesList>
{incident.updates.map(({status, body, updatedAt}, key) => (
<ListItem key={key}>
<UpdateHeading>
<StatusTitle>{startCase(status)}</StatusTitle>
<StatusDate>
{tct('([time])', {time: <TimeSince date={updatedAt} />})}
</StatusDate>
</UpdateHeading>
<Text dangerouslySetInnerHTML={{__html: marked(body)}} />
</ListItem>
))}
</UpdatesList>
</SidebarPanelItem>
))}
</SidebarPanel>
)}
</Fragment>
);
}
function getStatusSymbol(status: Status) {
return (
<Tooltip skipWrapper title={startCase(status)}>
{status === 'operational' ? (
<IconCheckmark size="sm" isCircled color="successText" />
) : status === 'major_outage' ? (
<IconFatal size="sm" color="errorText" />
) : status === 'degraded_performance' ? (
<IconWarning size="sm" color="warningText" />
) : status === 'partial_outage' ? (
<IconFire size="sm" color="warningText" />
) : (
<IconInfo size="sm" color="subText" />
)}
</Tooltip>
);
}
const AffectedServices = styled('div')`
margin: ${space(2)} 0;
`;
const UpdatesList = styled(List)`
gap: ${space(3)};
margin-left: ${space(1.5)};
position: relative;
&::before {
content: '';
display: block;
position: absolute;
height: 100%;
width: 2px;
margin: ${space(1)} 0 ${space(1)} -${space(1.5)};
background: ${p => p.theme.gray100};
}
&::after {
content: '';
display: block;
position: absolute;
bottom: -${space(1)};
margin-left: -${space(1.5)};
height: 30px;
width: 2px;
background: linear-gradient(
0deg,
${p => p.theme.background},
${p => color(p.theme.background).alpha(0).string()}
);
}
`;
const UpdateHeading = styled('div')`
margin-bottom: ${space(0.5)};
display: flex;
align-items: center;
gap: ${space(1)};
position: relative;
&::before {
content: '';
display: block;
position: absolute;
height: 8px;
width: 8px;
margin-left: -15px;
border-radius: 50%;
background: ${p => p.theme.purple300};
}
`;
const StatusTitle = styled('div')`
color: ${p => p.theme.headingColor};
font-weight: bold;
`;
const StatusDate = styled('div')`
color: ${p => p.theme.subText};
font-size: ${p => p.theme.fontSizeRelativeSmall};
`;
const ComponentList = styled(List)`
margin-top: ${space(1)};
display: block;
column-count: 2;
`;
const ComponentStatus = styled(ListItem)`
font-size: ${p => p.theme.fontSizeSmall};
line-height: 2;
`;
export default ServiceIncidents;