serviceIncidents.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import {Fragment, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import color from 'color';
  5. import sortBy from 'lodash/sortBy';
  6. import startCase from 'lodash/startCase';
  7. import {loadIncidents} from 'sentry/actionCreators/serviceIncidents';
  8. import Button from 'sentry/components/button';
  9. import List from 'sentry/components/list';
  10. import ListItem from 'sentry/components/list/listItem';
  11. import Text from 'sentry/components/text';
  12. import Tooltip from 'sentry/components/tooltip';
  13. import {IS_ACCEPTANCE_TEST} from 'sentry/constants';
  14. import {
  15. IconCheckmark,
  16. IconFatal,
  17. IconFire,
  18. IconInfo,
  19. IconOpen,
  20. IconWarning,
  21. } from 'sentry/icons';
  22. import {t, tct} from 'sentry/locale';
  23. import space from 'sentry/styles/space';
  24. import {SentryServiceStatus} from 'sentry/types';
  25. import marked from 'sentry/utils/marked';
  26. import TimeSince from '../timeSince';
  27. import SidebarItem from './sidebarItem';
  28. import SidebarPanel from './sidebarPanel';
  29. import SidebarPanelEmpty from './sidebarPanelEmpty';
  30. import SidebarPanelItem from './sidebarPanelItem';
  31. import {CommonSidebarProps, SidebarPanelKey} from './types';
  32. type Props = CommonSidebarProps;
  33. type Status =
  34. SentryServiceStatus['incidents'][number]['affectedComponents'][number]['status'];
  35. const COMPONENT_STATUS_SORT: Status[] = [
  36. 'operational',
  37. 'degraded_performance',
  38. 'partial_outage',
  39. 'major_outage',
  40. ];
  41. function ServiceIncidents({
  42. currentPanel,
  43. onShowPanel,
  44. hidePanel,
  45. collapsed,
  46. orientation,
  47. }: Props) {
  48. const [serviceStatus, setServiceStatus] = useState<SentryServiceStatus | null>(null);
  49. async function fetchData() {
  50. try {
  51. setServiceStatus(await loadIncidents());
  52. } catch (e) {
  53. Sentry.withScope(scope => {
  54. scope.setLevel('warning');
  55. scope.setFingerprint(['ServiceIncidents-fetchData']);
  56. Sentry.captureException(e);
  57. });
  58. }
  59. }
  60. useEffect(() => void fetchData(), []);
  61. // Never render incidents in acceptance tests
  62. if (IS_ACCEPTANCE_TEST) {
  63. return null;
  64. }
  65. if (!serviceStatus) {
  66. return null;
  67. }
  68. const active = currentPanel === SidebarPanelKey.ServiceIncidents;
  69. const isEmpty = !serviceStatus.incidents || serviceStatus.incidents.length === 0;
  70. if (isEmpty) {
  71. return null;
  72. }
  73. return (
  74. <Fragment>
  75. <SidebarItem
  76. id="statusupdate"
  77. orientation={orientation}
  78. collapsed={collapsed}
  79. active={active}
  80. icon={<IconWarning size="md" />}
  81. label={t('Service status')}
  82. onClick={onShowPanel}
  83. />
  84. {active && serviceStatus && (
  85. <SidebarPanel
  86. orientation={orientation}
  87. title={t('Recent service updates')}
  88. hidePanel={hidePanel}
  89. collapsed={collapsed}
  90. >
  91. {isEmpty && (
  92. <SidebarPanelEmpty>{t('There are no incidents to report')}</SidebarPanelEmpty>
  93. )}
  94. {serviceStatus.incidents.map(incident => (
  95. <SidebarPanelItem
  96. title={incident.name}
  97. key={incident.id}
  98. titleAction={
  99. <Button
  100. size="xs"
  101. icon={<IconOpen size="xs" />}
  102. priority="link"
  103. href={incident.url}
  104. external
  105. >
  106. {t('Full Incident Details')}
  107. </Button>
  108. }
  109. >
  110. <AffectedServices>
  111. {tct(
  112. "This incident started [timeAgo]. We're experiencing the following problems with our services",
  113. {
  114. timeAgo: (
  115. <strong>
  116. <TimeSince date={incident.createdAt} />
  117. </strong>
  118. ),
  119. }
  120. )}
  121. <ComponentList>
  122. {sortBy(incident.affectedComponents, i =>
  123. COMPONENT_STATUS_SORT.indexOf(i.status)
  124. ).map(({name, status}, key) => (
  125. <ComponentStatus
  126. key={key}
  127. padding="24px"
  128. symbol={getStatusSymbol(status)}
  129. >
  130. {name}
  131. </ComponentStatus>
  132. ))}
  133. </ComponentList>
  134. </AffectedServices>
  135. <UpdatesList>
  136. {incident.updates.map(({status, body, updatedAt}, key) => (
  137. <ListItem key={key}>
  138. <UpdateHeading>
  139. <StatusTitle>{startCase(status)}</StatusTitle>
  140. <StatusDate>
  141. {tct('([time])', {time: <TimeSince date={updatedAt} />})}
  142. </StatusDate>
  143. </UpdateHeading>
  144. <Text dangerouslySetInnerHTML={{__html: marked(body)}} />
  145. </ListItem>
  146. ))}
  147. </UpdatesList>
  148. </SidebarPanelItem>
  149. ))}
  150. </SidebarPanel>
  151. )}
  152. </Fragment>
  153. );
  154. }
  155. function getStatusSymbol(status: Status) {
  156. return (
  157. <Tooltip skipWrapper title={startCase(status)}>
  158. {status === 'operational' ? (
  159. <IconCheckmark size="sm" isCircled color="green300" />
  160. ) : status === 'major_outage' ? (
  161. <IconFatal size="sm" color="red300" />
  162. ) : status === 'degraded_performance' ? (
  163. <IconWarning size="sm" color="yellow300" />
  164. ) : status === 'partial_outage' ? (
  165. <IconFire size="sm" color="yellow300" />
  166. ) : (
  167. <IconInfo size="sm" color="gray300" />
  168. )}
  169. </Tooltip>
  170. );
  171. }
  172. const AffectedServices = styled('div')`
  173. margin: ${space(2)} 0;
  174. `;
  175. const UpdatesList = styled(List)`
  176. gap: ${space(3)};
  177. margin-left: ${space(1.5)};
  178. position: relative;
  179. &::before {
  180. content: '';
  181. display: block;
  182. position: absolute;
  183. height: 100%;
  184. width: 2px;
  185. margin: ${space(1)} 0 ${space(1)} -${space(1.5)};
  186. background: ${p => p.theme.gray100};
  187. }
  188. &::after {
  189. content: '';
  190. display: block;
  191. position: absolute;
  192. bottom: -${space(1)};
  193. margin-left: -${space(1.5)};
  194. height: 30px;
  195. width: 2px;
  196. background: linear-gradient(
  197. 0deg,
  198. ${p => p.theme.background},
  199. ${p => color(p.theme.background).alpha(0).string()}
  200. );
  201. }
  202. `;
  203. const UpdateHeading = styled('div')`
  204. margin-bottom: ${space(0.5)};
  205. display: flex;
  206. align-items: center;
  207. gap: ${space(1)};
  208. position: relative;
  209. &::before {
  210. content: '';
  211. display: block;
  212. position: absolute;
  213. height: 8px;
  214. width: 8px;
  215. margin-left: -15px;
  216. border-radius: 50%;
  217. background: ${p => p.theme.purple300};
  218. }
  219. `;
  220. const StatusTitle = styled('div')`
  221. color: ${p => p.theme.headingColor};
  222. font-weight: bold;
  223. `;
  224. const StatusDate = styled('div')`
  225. color: ${p => p.theme.subText};
  226. font-size: ${p => p.theme.fontSizeRelativeSmall};
  227. `;
  228. const ComponentList = styled(List)`
  229. margin-top: ${space(1)};
  230. display: block;
  231. column-count: 2;
  232. `;
  233. const ComponentStatus = styled(ListItem)`
  234. font-size: ${p => p.theme.fontSizeSmall};
  235. line-height: 2;
  236. `;
  237. export default ServiceIncidents;