mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
254 lines
6.8 KiB
TypeScript
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;
|