hostDetails.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import {ReactNode} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {useQuery} from '@tanstack/react-query';
  5. import CircleIndicator from 'sentry/components/circleIndicator';
  6. import {IconOpen} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {getDuration} from 'sentry/utils/formatters';
  10. import usePageFilters from 'sentry/utils/usePageFilters';
  11. import {
  12. getHostStatusBreakdownEventView,
  13. getHostStatusBreakdownQuery,
  14. } from 'sentry/views/starfish/modules/APIModule/queries';
  15. import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
  16. type Props = {
  17. host: string;
  18. };
  19. export function HostDetails({host}: Props) {
  20. const theme = useTheme();
  21. const pageFilter = usePageFilters();
  22. const {isLoading: isStatusBreakdownLoading, data: statusBreakdown} = useSpansQuery({
  23. queryString: getHostStatusBreakdownQuery({
  24. domain: host,
  25. datetime: pageFilter.selection.datetime,
  26. }),
  27. eventView: getHostStatusBreakdownEventView({
  28. domain: host,
  29. datetime: pageFilter.selection.datetime,
  30. }),
  31. initialData: [],
  32. });
  33. const hostMarketingName = Object.keys(EXTERNAL_APIS).find(key => host.includes(key));
  34. const failures = statusBreakdown?.filter((item: any) => item.status > 299);
  35. const successes = statusBreakdown?.filter((item: any) => item.status < 300);
  36. const totalCount = statusBreakdown?.reduce(
  37. (acc: number, item: any) => acc + item.count,
  38. 0
  39. );
  40. const externalApi = hostMarketingName && EXTERNAL_APIS[hostMarketingName];
  41. const {isLoading: isStatusLoading, data: statusData} = useQuery({
  42. queryKey: ['domain-status', host],
  43. queryFn: () =>
  44. fetch(`${externalApi?.statusPage}?format=json`).then(res => res.json()),
  45. retry: false,
  46. refetchOnWindowFocus: false,
  47. initialData: {},
  48. enabled: !!externalApi,
  49. });
  50. return (
  51. <DetailsContainer>
  52. <FlexContainer>
  53. {externalApi?.faviconLink && (
  54. <img
  55. src={externalApi.faviconLink}
  56. width="16"
  57. height="16"
  58. style={{marginRight: space(1)}}
  59. />
  60. )}
  61. {hostMarketingName ? (
  62. <span>
  63. <Host>{hostMarketingName}</Host>
  64. <span>{` (${host})`}</span>
  65. </span>
  66. ) : (
  67. <Host>{host}</Host>
  68. )}
  69. {!isStatusLoading && statusData.status ? (
  70. <StatusText>
  71. <CircleIndicator size={8} enabled={statusData.status.indicator === 'none'} />{' '}
  72. {statusData.status.description}
  73. </StatusText>
  74. ) : null}
  75. <LinkContainer>
  76. {externalApi?.statusPage && (
  77. <a href={externalApi.statusPage} target="_blank" rel="noreferrer">
  78. {t('Status')}
  79. <StyledIconOpen size="xs" />
  80. </a>
  81. )}
  82. </LinkContainer>
  83. </FlexContainer>
  84. <ExternalApiDescription>{externalApi?.description}</ExternalApiDescription>
  85. <StatusContainer>
  86. {isStatusBreakdownLoading
  87. ? null
  88. : failures?.map((item: any) => {
  89. const errorCodeDescription = ERROR_CODE_DESCRIPTIONS[item.status];
  90. return (
  91. <MeterBarContainer key={item.status}>
  92. <MeterBar
  93. color={theme.red300}
  94. meterItems={['count']}
  95. minWidth={0.1}
  96. row={item}
  97. total={totalCount}
  98. meterText={
  99. <Failure>{`${item.status}${
  100. errorCodeDescription ? ` ${errorCodeDescription}` : ''
  101. } (${item.count})`}</Failure>
  102. }
  103. />
  104. </MeterBarContainer>
  105. );
  106. })}
  107. {isStatusBreakdownLoading
  108. ? null
  109. : successes?.map((item: any) => (
  110. <MeterBarContainer key={item.status}>
  111. <MeterBar
  112. color={theme.green300}
  113. meterItems={['count']}
  114. minWidth={0.1}
  115. row={item}
  116. total={totalCount}
  117. meterText={`${item.status} (${item.count})`}
  118. />
  119. </MeterBarContainer>
  120. ))}
  121. </StatusContainer>
  122. </DetailsContainer>
  123. );
  124. }
  125. const DetailsContainer = styled('div')`
  126. padding: ${space(2)};
  127. border-radius: ${p => p.theme.borderRadius};
  128. border: 1px solid ${p => p.theme.border};
  129. margin-bottom: ${space(2)};
  130. `;
  131. const FlexContainer = styled('div')`
  132. display: flex;
  133. flex-direction: row;
  134. `;
  135. const Host = styled('span')`
  136. font-weight: bold;
  137. `;
  138. const StatusText = styled('span')`
  139. margin-left: ${space(2)};
  140. `;
  141. const StyledIconOpen = styled(IconOpen)`
  142. flex: 0;
  143. top: 2px;
  144. position: relative;
  145. margin-left: ${space(0.5)};
  146. `;
  147. const LinkContainer = styled('span')`
  148. flex: 1;
  149. text-align: right;
  150. `;
  151. const StatusContainer = styled('span')`
  152. margin-top: ${space(1)};
  153. flex: 1;
  154. height: 20px;
  155. display: flex;
  156. flex-direction: row;
  157. gap: ${space(1)};
  158. `;
  159. const MeterBarContainer = styled('div')`
  160. width: 150px;
  161. top: -6px;
  162. position: relative;
  163. `;
  164. const Failure = styled('span')`
  165. font-weight: bold;
  166. color: ${p => p.theme.red300};
  167. `;
  168. const ExternalApiDescription = styled('span')`
  169. font-size: ${p => p.theme.fontSizeSmall};
  170. color: ${p => p.theme.gray300};
  171. `;
  172. const ERROR_CODE_DESCRIPTIONS = {
  173. 400: 'Bad Request',
  174. 401: 'Unauthorized',
  175. 403: 'Forbidden',
  176. 404: 'Not Found',
  177. 405: 'Method Not Allowed',
  178. 408: 'Request Timeout',
  179. 429: 'Too Many Requests',
  180. 500: 'Internal Server Error',
  181. };
  182. const EXTERNAL_APIS = {
  183. stripe: {
  184. statusPage: 'https://status.stripe.com/',
  185. faviconLink: 'https://stripe.com/favicon.ico',
  186. description: t(
  187. 'Stripe is a suite of payment APIs that powers commerce for online businesses of all sizes'
  188. ),
  189. },
  190. twilio: {
  191. statusPage: 'https://status.twilio.com/',
  192. faviconLink: 'https://www.twilio.com/favicon.ico',
  193. description: t('Twilio is a cloud communications platform as a service company.'),
  194. },
  195. sendgrid: {
  196. statusPage: 'https://status.sendgrid.com/',
  197. faviconLink: 'https://sendgrid.com/favicon.ico',
  198. description: t(
  199. 'SendGrid is a cloud-based SMTP provider that allows you to send email without having to maintain email servers.'
  200. ),
  201. },
  202. plaid: {
  203. statusPage: 'https://status.plaid.com/',
  204. faviconLink: 'https://plaid.com/favicon.ico',
  205. description: t(
  206. 'Plaid is a technology platform that enables applications to connect with users bank accounts.'
  207. ),
  208. },
  209. paypal: {statusPage: 'https://www.paypal-status.com/'},
  210. braintree: {statusPage: 'https://status.braintreepayments.com/'},
  211. clickup: {
  212. statusPage: 'https://clickup.statuspage.io/',
  213. faviconLink: 'https://clickup.com/favicon.ico',
  214. description: t(
  215. 'ClickUp is a productivity platform that provides a fundamentally new way to work.'
  216. ),
  217. },
  218. github: {
  219. statusPage: 'https://www.githubstatus.com/',
  220. faviconLink: 'https://github.com/favicon.ico',
  221. description: t(
  222. 'GitHub is a provider of Internet hosting for software development and version control.'
  223. ),
  224. },
  225. gitlab: {
  226. statusPage: 'https://status.gitlab.com/',
  227. faviconLink: 'https://gitlab.com/favicon.ico',
  228. description: t(
  229. 'GitLab is a web-based DevOps lifecycle tool that provides a Git-repository manager providing wiki, issue-tracking and CI/CD pipeline features.'
  230. ),
  231. },
  232. bitbucket: {
  233. statusPage: 'https://bitbucket.status.atlassian.com/',
  234. faviconLink: 'https://bitbucket.org/favicon.ico',
  235. description: t(
  236. 'Bitbucket is a web-based version control repository hosting service.'
  237. ),
  238. },
  239. jira: {
  240. statusPage: 'https://jira.status.atlassian.com/',
  241. faviconLink: 'https://jira.com/favicon.ico',
  242. description: t(
  243. 'Jira is a proprietary issue tracking product developed by Atlassian.'
  244. ),
  245. },
  246. asana: {
  247. statusPage: 'https://trust.asana.com/',
  248. faviconLink: 'https://asana.com/favicon.ico',
  249. description: t(
  250. 'Asana is a web and mobile application designed to help teams organize, track, and manage their work.'
  251. ),
  252. },
  253. trello: {statusPage: 'https://trello.status.atlassian.com/'},
  254. zendesk: {statusPage: 'https://status.zendesk.com/'},
  255. intercom: {statusPage: 'https://www.intercomstatus.com/'},
  256. freshdesk: {statusPage: 'https://status.freshdesk.com/'},
  257. linear: {statusPage: 'https://status.linear.app/'},
  258. gaussMoney: {},
  259. };
  260. export const INTERNAL_API_REGEX = /\d\.\d|localhost/;
  261. export function MeterBar({
  262. minWidth,
  263. meterItems,
  264. row,
  265. total,
  266. color,
  267. meterText,
  268. }: {
  269. color: string;
  270. meterItems: string[];
  271. minWidth: number;
  272. row: any;
  273. total: number;
  274. meterText?: ReactNode;
  275. }) {
  276. const widths = [] as number[];
  277. meterItems.reduce((acc, item, index) => {
  278. const width = Math.max(
  279. Math.min(
  280. (100 * row[item]) / total - acc,
  281. 100 - acc - minWidth * (meterItems.length - index)
  282. ),
  283. minWidth
  284. );
  285. widths.push(width);
  286. return acc + width;
  287. }, 0);
  288. return (
  289. <span>
  290. <MeterText>
  291. {meterText ?? `${getDuration(row[meterItems[0]] / 1000, 0, true, true)}`}
  292. </MeterText>
  293. <MeterContainer width={100}>
  294. <Meter width={widths[0]} color={color} />
  295. </MeterContainer>
  296. </span>
  297. );
  298. }
  299. const MeterContainer = styled('span')<{width: number}>`
  300. display: flex;
  301. width: ${p => p.width}%;
  302. height: ${space(1)};
  303. background-color: ${p => p.theme.gray100};
  304. margin-bottom: 4px;
  305. `;
  306. const Meter = styled('span')<{
  307. color: string;
  308. width: number;
  309. }>`
  310. display: block;
  311. width: ${p => p.width}%;
  312. height: 100%;
  313. background-color: ${p => p.color};
  314. `;
  315. const MeterText = styled('span')`
  316. font-size: ${p => p.theme.fontSizeExtraSmall};
  317. color: ${p => p.theme.gray300};
  318. white-space: nowrap;
  319. `;